Description
@payloadcms/db-vercel-postgres fails to execute large SQL queries on Vercel when using NeonDB. The same queries work fine locally and through raw pg.Pool.
Our pages collection has 31 block types in a layout blocks field. Payload + Drizzle generates a ~68KB SQL statement with 30+ lateral joins to query this collection. This query:
- ✅ Executes in 259ms on NeonDB dashboard (simple query protocol)
- ✅ Executes in 177ms through raw
pg.Pool (TCP, named prepared statement)
- ✅ Works locally with
postgresAdapter (pg.Pool over TCP)
- ✅ Works locally with
vercelPostgresAdapter (same connection string)
- ❌ Fails on Vercel with
vercelPostgresAdapter — times out waiting for NeonDB response, even with 60s function timeout
Switching from vercelPostgresAdapter to postgresAdapter on Vercel immediately fixes the issue.
Root Cause
vercelPostgresAdapter uses @vercel/postgres → @neondatabase/serverless, which sends queries over WebSocket rather than TCP. The 68KB SQL payload appears to hit protocol-level limits or buffering issues in the WebSocket transport that TCP handles without problems.
Tested with both pooled (-pooler hostname) and unpooled (direct) NeonDB connections — both fail on Vercel with vercelPostgresAdapter.
Secondary Issue: Connection Pool Leak in connectWithReconnect
While investigating, we also found that connectWithReconnect in both @payloadcms/db-postgres and @payloadcms/db-vercel-postgres calls pool.connect() to check out a client but never calls result.release() to return it to the pool:
// connect.js (db-postgres v3.81.0, line 4-37)
const connectWithReconnect = async function({ adapter, pool, reconnect = false }) {
let result;
if (!reconnect) {
result = await pool.connect(); // ← checks out a client
}
// ...
result.prependListener('error', (err) => { /* ... */ });
// ← result.release() is NEVER called
};
This permanently holds one connection from the pool. With the default max: 5, this leaves only 4 connections for actual queries — exacerbating pool exhaustion when Payload runs multiple concurrent queries per find() call (selectDistinct + findMany + countDistinct, multiplied by depth > 0 relationship population).
Environment
- Payload: 3.81.0
@payloadcms/db-vercel-postgres: 3.79.1
@payloadcms/db-postgres: 3.81.0
- Drizzle ORM: 0.44.7
- Next.js: 15.4.10
- Database: NeonDB (serverless PostgreSQL, eu-central-1)
- Hosting: Vercel (Node.js runtime, not edge)
Workaround
Use postgresAdapter (from @payloadcms/db-postgres) instead of vercelPostgresAdapter on Vercel. This uses pg.Pool with TCP connections, which handles large queries without issues. vercelPostgresAdapter is only required for Edge Runtime.
Also increase pool.max from the default — with 31 block types and depth > 0, Payload can generate many concurrent sub-queries that exhaust a small pool, especially with the leaked connection from connectWithReconnect.
// payload.config.ts
import { postgresAdapter } from '@payloadcms/db-postgres'
db: postgresAdapter({
pool: {
connectionString: process.env.POSTGRES_URL || '',
max: 20, // default is too low for collections with many blocks
},
push: false,
})
Steps to Reproduce
- Create a
pages collection with 20+ block types in a layout blocks field
- Configure
vercelPostgresAdapter with NeonDB
- Deploy to Vercel (Node.js runtime)
- Request a page — the query times out
Expected: Query completes successfully (as it does with postgresAdapter)
Actual: Connection to NeonDB times out via WebSocket transport
Description
@payloadcms/db-vercel-postgresfails to execute large SQL queries on Vercel when using NeonDB. The same queries work fine locally and through rawpg.Pool.Our
pagescollection has 31 block types in alayoutblocks field. Payload + Drizzle generates a ~68KB SQL statement with 30+ lateral joins to query this collection. This query:pg.Pool(TCP, named prepared statement)postgresAdapter(pg.Pool over TCP)vercelPostgresAdapter(same connection string)vercelPostgresAdapter— times out waiting for NeonDB response, even with 60s function timeoutSwitching from
vercelPostgresAdaptertopostgresAdapteron Vercel immediately fixes the issue.Root Cause
vercelPostgresAdapteruses@vercel/postgres→@neondatabase/serverless, which sends queries over WebSocket rather than TCP. The 68KB SQL payload appears to hit protocol-level limits or buffering issues in the WebSocket transport that TCP handles without problems.Tested with both pooled (
-poolerhostname) and unpooled (direct) NeonDB connections — both fail on Vercel withvercelPostgresAdapter.Secondary Issue: Connection Pool Leak in
connectWithReconnectWhile investigating, we also found that
connectWithReconnectin both@payloadcms/db-postgresand@payloadcms/db-vercel-postgrescallspool.connect()to check out a client but never callsresult.release()to return it to the pool:This permanently holds one connection from the pool. With the default
max: 5, this leaves only 4 connections for actual queries — exacerbating pool exhaustion when Payload runs multiple concurrent queries perfind()call (selectDistinct + findMany + countDistinct, multiplied by depth > 0 relationship population).Environment
@payloadcms/db-vercel-postgres: 3.79.1@payloadcms/db-postgres: 3.81.0Workaround
Use
postgresAdapter(from@payloadcms/db-postgres) instead ofvercelPostgresAdapteron Vercel. This usespg.Poolwith TCP connections, which handles large queries without issues.vercelPostgresAdapteris only required for Edge Runtime.Also increase
pool.maxfrom the default — with 31 block types and depth > 0, Payload can generate many concurrent sub-queries that exhaust a small pool, especially with the leaked connection fromconnectWithReconnect.Steps to Reproduce
pagescollection with 20+ block types in alayoutblocks fieldvercelPostgresAdapterwith NeonDBExpected: Query completes successfully (as it does with
postgresAdapter)Actual: Connection to NeonDB times out via WebSocket transport