DEV Community

Cover image for prepared statement s0 already exists: Fix for Prisma +
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

prepared statement s0 already exists: Fix for Prisma +

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 directUrl to the Prisma datasource and configure the pooled URL with pgbouncer=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") }) }) })
Enter fullscreen mode Exit fullscreen mode

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:

# .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"
Enter fullscreen mode Exit fullscreen mode

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/schema.prisma
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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'],
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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


Originally published at https://www.iloveblogs.blog

Top comments (0)