Supabase

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.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

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:

text
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:

  1. Prisma executes a query on connection A and implicitly prepares s0.
  2. The pooler releases connection A back to the pool after the transaction.
  3. The next Prisma query is handed to connection B. Prisma tries to use s0 again, but connection B already has a prepared statement named s0 (from an earlier transaction) or expects a fresh preparation. PostgreSQL throws 42P05 — 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:

bash
# .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=true lets 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=1 prevents Prisma from opening redundant connections inside the pool.
  • DIRECT_URL connects straight to PostgreSQL’s primary port 5432, bypassing the pooler. This is required for commands like prisma migrate deploy and prisma 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
// 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:

bash
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:

ts
// 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:

ts
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:

bash
npm install @prisma/client@6 prisma@6
npx prisma generate

Pin 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

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote