Skip to content

vercelPostgresAdapter fails on large queries (68KB+ SQL with 30+ lateral joins) #16256

@ElForastero

Description

@ElForastero

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

  1. Create a pages collection with 20+ block types in a layout blocks field
  2. Configure vercelPostgresAdapter with NeonDB
  3. Deploy to Vercel (Node.js runtime)
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions