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.
-
Symptom:
Error during query execution: … prepared statement "s0" already exists(sometimes "s8" or another number) - Root cause: Transaction‑level pooling re‑uses connections, causing prepared statement name conflicts
-
Fix: Add
directUrlto the Prisma datasource and configure the pooled URL withpgbouncer=true&connection_limit=1 - Verification: Run the same Prisma query that originally failed; it returns data without the error
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 generate
Quick 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 latest Prisma release where the parameter is fully supported:
npm install @prisma/client@latest prisma@latest
npx prisma generate
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.
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.
Related
- Fix Prisma Query Engine Library Not Found
- Prisma: Environment variable not found: DATABASE_URL
- Postgres INSERT If Not Exists: Fix Duplicate Key Violations
- Next.js + Supabase: 10 Production Mistakes + Fixes (2026)
Originally published at https://www.iloveblogs.blog
Top comments (0)