VibeRaven

Why does my Vercel app hit "Max client connections reached" on Supabase?

By Ohad Krispin, founder of VibeRaven · LinkedIn

Usually because the app connects through Supabase's shared pooler in session mode: the connection string with a pooler.supabase.com host on port 5432. In session mode each client keeps its own database connection, and Supabase caps the clients at the Pool Size in Database Settings, so a few warm Vercel function instances, each with its own client pool, use it up. Supabase's connection guide says serverless and edge functions should use the shared pooler in transaction mode instead: the same host on port 6543, the client created once per instance, and prepared statements turned off (prepare: false for Postgres.js and Drizzle, pgbouncer=true for Prisma). Keep a 5432 string for migrations. If the error continues on 6543, the app opens more clients than the pooler allows for your compute size: lower the pool size per instance, or move to a larger compute size.

Why this matters

This page was checked against Supabase and Vercel docs read on 14 October 2026; no Supabase project or Vercel deployment was load tested for it. The error comes only from code that talks to Postgres through a driver or ORM, such as Prisma, Drizzle, Postgres.js or node-postgres. supabase-js goes through the Data API over HTTPS, which Supabase says has its own built-in pooler. A driver keeps its own pool on top of Supabase's poolers: Supabase's docs say Postgres.js defaults to 10 connections and Prisma to num_cpus * 2 + 1, for every warm instance of a function, and the number of warm instances is not something you control. Supabase's Prisma quickstart puts the 5432 session pooler string in DATABASE_URL and switches to the 6543 string only for serverless deployments, so an app set up from the first half works on a laptop, where one process holds one pool, and fails once traffic spreads over many function instances. Supabase's Supavisor FAQ says session mode can queue clients for up to a minute, and its Prisma troubleshooting page lists "Max client connections reached" for clients past the Pool Size.

What we do not know yet

  • Which pool size to set per function instance. Supabase's connection guide says to set the pool to 1 connection in a serverless function and raise it only when invocations queue for it. Vercel's connection pooling guide says to avoid a max pool size of 1 on Fluid compute, which Vercel has enabled by default for new projects since April 23, 2025, because it does not reduce total connections and harms concurrency. Neither publishes a measurement against Supabase's pooler, and we did not load test either setting.
  • Whether Postgres.js is safe on transaction mode in your app. Supabase's Postgres.js guide warns that it pipelines queries by default, that with transaction mode this can hang queries or return mismatched rows, and that it has no working option to turn pipelining off. We did not reproduce this or test other drivers for it.
  • What your project's limits are. The pooler's max clients and Postgres max connections depend on the compute size, in the table on Supabase's compute and disk page. This page uses no numbers from a real project.

Find the connection string, then fix it

  • Find the code that opens a Postgres connection: a Prisma schema, a Drizzle or Postgres.js client, or a node-postgres Pool. If the app reaches Supabase only through supabase-js, this fix does not apply to its code; look for other clients connecting to the database, such as a script, a scheduled job or a database GUI.
  • Read the host and port of the DATABASE_URL your Vercel Production environment uses, not the one in .env.local. Supabase's connection guide maps them like this:
    db.[PROJECT-REF].supabase.co:5432              direct connection, IPv6 unless the IPv4 add-on is on
    aws-[INDEX]-[REGION].pooler.supabase.com:5432  shared pooler, session mode
    aws-[INDEX]-[REGION].pooler.supabase.com:6543  shared pooler, transaction mode
    db.[PROJECT-REF].supabase.co:6543              dedicated pooler, transaction mode, paid plans
  • If it is the direct connection, switch anyway: Supabase lists Vercel among the platforms that only accept IPv4 connections, the direct connection is IPv6 unless the project has the IPv4 add-on, and Supabase's guide keeps it for persistent backends, migrations and pg_dump.
  • Copy the Transaction pooler string from the Connect dialog in the Supabase Dashboard rather than changing the port by hand. Supabase says the pooler host cannot be worked out from your region and that shared pooler connections use the username postgres.[PROJECT-REF]. Set it as DATABASE_URL in the Vercel project, then redeploy: Vercel applies environment variable changes only to new deployments.
  • Turn off prepared statements, which transaction mode does not support. Otherwise queries fail with the "prepared statement already exists" error that Supabase's Prisma troubleshooting page covers. Each driver does it differently:
    // Postgres.js and Drizzle
    const sql = postgres(process.env.DATABASE_URL!, { prepare: false })
    
    // Prisma: add pgbouncer=true to the 6543 string
    // DATABASE_URL="postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@[POOLER-HOST]:6543/postgres?pgbouncer=true"
    
    // node-postgres: leave out the name field, which makes a prepared statement
    await pool.query({ text: 'select body from notes where id = $1', values: [id] })
  • Keep migrations off the transaction pooler. Supabase's connection guide sends migrations to the direct connection; its Prisma quickstart puts the 5432 session pooler string in DIRECT_URL and points prisma.config.ts at it for the Prisma CLI, while the app reads the 6543 string.
    DATABASE_URL="postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@[POOLER-HOST]:6543/postgres?pgbouncer=true"
    DIRECT_URL="postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@[POOLER-HOST]:5432/postgres"
  • Create the client once, at module scope, so every request on a warm instance shares it, and keep its pool small. Supabase says to start at 1 connection per instance. On Fluid compute, Vercel says not to cap a pool at 1, to keep a short idle timeout, and to register the pool with attachDatabasePool from @vercel/functions so idle connections close before the instance suspends. Whatever you pick, the pool size times the instances that can be warm at once has to stay under the pooler's client limit for your compute size. The max below is an example, not a recommendation:
    import { Pool } from 'pg'
    import { attachDatabasePool } from '@vercel/functions'
    
    export const pool = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 3,
      idleTimeoutMillis: 5000,
    })
    
    attachDatabasePool(pool)
  • Check the result after the deploy, under the traffic that caused the error. In the Supabase Dashboard, Observability has a Shared Pooler (Supavisor) Client Connections report, which Supabase notes is not real-time. For a live count of database connections, Supabase's pooling guide gives this query for the SQL Editor:
    select
      count(usename),
      count(application_name),
      application_name,
      usename
    from
      pg_stat_ssl
      join pg_stat_activity on pg_stat_ssl.pid = pg_stat_activity.pid
    group by usename, application_name;
  • Read a new error as the next step, not as a failed fix. "prepared statement already exists" means a driver still prepares statements. "Tenant or user not found" is the pooler failing to match your host and username to a project, which Supabase's guide lists under troubleshooting: copy the string again from the Connect dialog.
  • If "Max client connections reached" continues on 6543, the app has more clients than your compute size allows. Lower each instance's pool (Supabase suggests serverless setups start Prisma at connection_limit=1), close connections the app no longer needs, or move to a larger compute size, which Supabase says also raises the max client limit. Raising Pool Size lets the pooler open more Postgres connections, and Supabase says direct connections plus both poolers' backend connections have to stay below Postgres max connections, with room left for Auth, Storage and PostgREST.

Optional repo check

For AI-built Vercel + Supabase apps, npx -y viberaven@1.6.8 check reads the repo for the port part of this, as pooler_port_mismatch. It looks only when package.json, a Prisma schema or an import shows a Postgres driver or ORM, then reads DATABASE_URL and POSTGRES_URL lines for a port. In runs on six fixture repos on 2026-10-14 it warned on a session pooler string on 5432 and on a direct db.[PROJECT-REF].supabase.co:5432 string, calling both direct 5432 evidence, so it does not tell those two apart. It passed a transaction pooler string on 6543 next to a DIRECT_URL on 5432, and called a repo that uses only supabase-js not applicable. It also passed a 6543 string with Postgres.js left on prepared statements, because it does not read driver options, and with no env file in the repo it warned that it found no 6543 evidence. It reads files, not the environment variables set in your Vercel project or your Supabase pool settings. It is advice, not a gate, and a repository check, not a live database or security test. It exits with code 1 when it finds a blocker and writes a .viberaven folder that it does not add to .gitignore.

Related guides

Sources

Should I raise the Pool Size instead?

Not as the first fix. In session mode the Pool Size caps clients for each database role and database combination, so raising it lets more function instances in, but each one still holds a real Postgres connection while it stays connected. Supabase's pooling guide says direct connections plus the backend connections of both poolers have to stay below the maximum for your compute size, and Supabase's own services use some of that. Transaction mode shares connections between clients, which is why Supabase recommends it for serverless functions.

Do I need a pooler string if my app only uses supabase-js?

No. supabase-js calls the Data API over HTTPS, and Supabase says that API has a built-in connection pooler and suits serverless workloads. A connection string matters only for code that talks to Postgres directly, such as Prisma, Drizzle, Postgres.js or node-postgres.

Why did it work locally?

Locally one long-running process holds one client pool. On Vercel every warm function instance creates its own pool, and Supabase's connection guide notes that a few dozen instances with Postgres.js's default of 10 connections is enough to exhaust the pool. Traffic, not your code, decides how many instances run.

Can I use the direct connection from Vercel?

Supabase lists Vercel among the platforms that only accept IPv4 connections, and the direct connection is IPv6 unless the project has the IPv4 add-on, which Supabase offers on Pro plans and above. Even with the add-on, a direct connection has no pooler in front of it, and Supabase's guide keeps it for persistent backends, migrations and pg_dump.