prepared statement s0 already exists: Fix for Prisma +
Solve the Prisma 'prepared statement s0 already exists' error caused by Supabase pooling. Add directUrl, enable pgbouncer mode, and verify your connection.
TL;DR#
If you're seeing … prepared statement "s0" already exists, the cause is a prepared‑statement collision inside a connection pooler such as Supabase’s PgBouncer/Supavisor. Prisma prepares a statement on one backend connection, the pooler hands the next query to a different connection that already has a statement with the same name, and PostgreSQL returns error code SQLSTATE 42P05.
The permanent fix is to add a directUrl to your datasource and configure pgbouncer=true in your pooled connection string. If you just need the error gone right now, restarting the Supabase project clears the prepared‑statement cache.
The error, decoded#
The exact symptom, as reported in a Stack Overflow question with 12 upvotes and over 12 000 views, looks like this:
error: PrismaClientUnknownRequestError:
Invalid `prisma.receipts.findMany()` invocation:
Error occurred during query execution:
ConnectorError(ConnectorError { user_facing_error: None,
kind: QueryError(Error { kind: Db,
cause: Some(DbError { severity: "ERROR",
parsed_severity: Some(Error), code: SqlState("42P05"),
message: "prepared statement \"s0\" already exists",
detail: None, hint: None, position: None, where_: None,
schema: None, table: None, column: None,
datatype: None, constraint: None,
file: Some("prepare.c"), line: Some(480),
routine: Some("StorePreparedStatement") }) }) })The statement name varies; the same query can fail with "s8", "s1", or any other sequential number. Many Prisma queries work fine individually, but once the pool reuses a connection the error returns. Restarting the Next.js dev server (yarn dev) temporarily clears it because the old connections are closed and a fresh set is established—until the pool shuffles them again.
The error appears consistently in environments that use transaction‑level connection pooling—Supabase with PgBouncer or Supavisor, especially when pgbouncer=true is missing from the connection string or no directUrl is provided.
Why pooling breaks prepared statements#
PostgreSQL’s PREPARE mechanism creates a named, pre‑parsed statement on a specific backend connection. When you use a connection pooler such as Supavisor in transaction mode (the default on Supabase), every transaction may land on a different backend connection. Here’s the sequence that causes the error:
- Prisma executes a query on connection A and implicitly prepares
s0. - The pooler releases connection A back to the pool after the transaction.
- The next Prisma query is handed to connection B. Prisma tries to use
s0again, but connection B already has a prepared statement nameds0(from an earlier transaction) or expects a fresh preparation. PostgreSQL throws42P05— prepared statement already exists.
This is not a Prisma bug; it’s the documented behaviour of all statement‑aware ORMs behind a transaction‑level pooler. The same problem occurs with drizzle-orm or raw pg unless the client knows it’s behind a pooler and either disables prepared statements or keeps a deduplication map. Prisma can be told about the pooler through its connection string parameters.
The fix: Add directUrl and tune your connection string#
Three concrete changes eliminate the error permanently without sacrificing performance.
1. Add environment variables#
Create (or update) your .env file with two separate URLs:
# .env
# Pooled connection string — used by Prisma Client
DATABASE_URL="postgresql://postgres.[YOUR-PROJECT-REF]:[YOUR-PASSWORD]@aws-0-us-west-1.pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1"
# Direct connection string — used for migrations and introspection
DIRECT_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres"Here’s what each part does:
pgbouncer=truelets Prisma know it’s behind a PgBouncer‑compatible pooler. Prisma adjusts its protocol level to avoid session‑specific state (including prepared statements that can’t be shared).connection_limit=1prevents Prisma from opening redundant connections inside the pool.DIRECT_URLconnects straight to PostgreSQL’s primary port 5432, bypassing the pooler. This is required for commands likeprisma migrate deployandprisma db push, which must hold a stable, long‑lived connection.
If you’ve ever hit the DATABASE_URL environment variable not being found, check Prisma: Environment variable not found: DATABASE_URL — it’s easy to miss that you now have two env vars.
2. Update the datasource block#
Modify your prisma/schema.prisma to include the directUrl property:
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}Without directUrl, every Prisma command — including migrations — goes through the pooler, which will reliably break schema‑modifying operations. The directUrl tells Prisma to use the direct connection whenever it runs something that requires a consistent session.
After editing the schema, regenerate the client so it picks up the new configuration:
npx prisma generateQuick relief: Restart the Supabase project#
If you can’t apply the fix immediately, restarting the Supabase project clears all backend sessions and prepared statements. Go to Settings → Restart project. The next query will work because the prepared‑statement cache is empty.
This is a temporary workaround; the error returns as soon as the pool recycles connections.
Verify the fix#
Run the exact Prisma query that previously triggered the error. In a Next.js API route or server component:
// app/api/receipts/route.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function GET() {
const rows = await prisma.receipts.findMany({
where: { /* your conditions */ },
orderBy: { included_in_block_timestamp: 'desc' },
take: 2,
})
return Response.json(rows)
}Hit the endpoint repeatedly (or refresh a page that calls it) and watch the terminal. No 42P05 errors appear. If you’re in development, enable Prisma’s query logging temporarily to confirm that prepared statements are not being attempted:
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient({
log: ['query'],
})What if the error still appears?#
Two common edge cases:
You forgot to run prisma generate after editing the schema. The client stays compiled with the old datasource block. Regenerate and restart the server.
You are using an older version of Prisma that doesn’t respect pgbouncer. Upgrade to the newest release of the major version your project already uses, 6 for the url and directUrl datasource shown on this page:
npm install @prisma/client@6 prisma@6
npx prisma generatePin the major rather than @latest, which on 6 October 2026 installs the prisma 8.0.0-rc.20 CLI, a release candidate without generate, next to @prisma/client 7.10.0 (npm registry).
If all else fails, you can force Prisma to disable prepared statements entirely by appending statement_cache_size=0 to your pooled URL. This sacrifices performance but guarantees no statement conflicts on any pooler configuration. Use it only as a last resort — pgbouncer=true is the proper fix.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
FAQ#
Do I need connection_limit=1 even with directUrl?
Yes, for the pooled URL only. The direct URL bypasses the pooler, so connection limits there are governed by PostgreSQL’s own max_connections. The pooler itself is most efficient when Prisma opens exactly one connection inside the pool, which is why Supabase recommends connection_limit=1.
Does this error happen on platforms other than Supabase?
Yes. Any PostgreSQL deployment that uses a transaction‑mode PgBouncer or similar pooler (Heroku, DigitalOcean, etc.) can produce the same error. The fix—adding pgbouncer=true and a direct URL for migrations—applies universally.
Can I avoid prepared statements altogether?
You can append ?statement_cache_size=0 to your pooled URL, but every query then goes through parse‑plan‑execute every time, adding latency. Stick with pgbouncer=true unless you have a very unusual use case.
Further reading: Fix npx prisma db push Hanging on Supabase (Port 6543), Prisma Migrate Dev Hangs? Fix the Supabase Pooler Stall.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Prisma: Environment variable not found: DATABASE_URL
Prisma errors out with missing DATABASE_URL when the .env file isn't loaded. Trigger prisma generate and get your GraphQL query running again.
Prisma Migrate Dev Hangs? Fix the Supabase Pooler Stall
Reproduced on Supavisor: migrate dev waits at an early query on 6543 and migrates in a second on 5432. The fix for Prisma 6 and 7, and why the advisory lock is not the cause.
Fix npx prisma db push Hanging on Supabase (Port 6543)
Reproduced against Supavisor: the same schema waits until killed on port 6543 and syncs on 5432. The port, directUrl and prisma.config.ts fixes, version by version.


