Serverless
A serverless function is a normal process that is frozen between invocations, killed without warning, and multiplied by traffic. Entities and queries do not change; only where you build the pool and how you size it.
Put the pool at module scope
Section titled “Put the pool at module scope”Platforms reuse a warm instance for consecutive requests, and module scope runs once per instance:
import { PgQuerierPool } from 'uql-orm/postgres';import './entities.js';
export const pool = new PgQuerierPool({ connectionString: process.env.DATABASE_URL, max: 2, connectionTimeoutMillis: 5_000,});A pool built inside the handler pays a handshake on every request. Nothing connects until the first query, so an unused pool costs nothing.
Always set connectionTimeoutMillis. Without it, a database that is asleep, unreachable or full keeps the request waiting (and billed) until the platform kills it; with it, the query throws in time to return a 503.
A framework dev server re-runs the module on every hot reload, so cache the pool on globalThis in development (see Next.js).
Size max per instance
Section titled “Size max per instance”max: 10 on a platform that scales to 200 instances asks for 2,000 connections, and a small Postgres accepts about 100. Size max for the concurrency inside one instance. Lambda runs one invocation per environment, so 1 or 2 is right unless your handler runs queries in parallel (a Promise.all over three wants 3). Runtimes that share an instance across requests, such as Vercel Fluid compute, want a handful. Even max: 1 is safe with transactions, since pool calls inside one reuse its connection.
When instance count alone can exhaust the server, put a pooler in front of the database rather than shrinking max, and run migrations against the direct endpoint.
The first query after a thaw can fail
Section titled “The first query after a thaw can fail”A frozen instance runs no timers, so it cannot notice a connection going away. Meanwhile a database idle_session_timeout, a NAT or a pooler drops the socket, and the next invocation’s first query fails with ECONNRESET or Connection terminated unexpectedly. UQL discards the dead client and the next acquire opens a fresh one. Retry that request yourself, on connection errors only, and never a non-idempotent write outside a transaction.
AWS Lambda
Section titled “AWS Lambda”The module-scope pool above is the whole setup:
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';import { pool } from './db.js';import { Invoice } from './entities.js';
export const handler: APIGatewayProxyHandlerV2 = async () => { const invoices = await pool.findMany(Invoice, { $where: { paid: false }, $limit: 20, }); return { statusCode: 200, body: JSON.stringify(invoices) };};Do not call pool.end() per invocation: Lambda freezes the process once the handler settles, so it would not finish, and the database reclaims connections when the environment dies. A callback-style handler needs context.callbackWaitsForEmptyEventLoop = false, or the idle socket keeps the invocation open until it times out. In a VPC, put RDS Proxy next to RDS, or instance count becomes connection count.
Vercel
Section titled “Vercel”On Fluid compute an instance serves several requests and then suspends. attachDatabasePool releases idle clients before that, so connections follow traffic rather than instance count:
import { attachDatabasePool } from '@vercel/functions';import { PgQuerierPool } from 'uql-orm/postgres';
export const pool = new PgQuerierPool({ connectionString: process.env.DATABASE_URL, max: 5,});
attachDatabasePool(pool.pool);pool.pool is the driver’s pool. Keep the Node.js runtime, since pg needs TCP. An Astro app can also cache responses at the edge with cacheVercel(), so repeat requests never reach a function.
uql-orm/neon swaps pg for @neondatabase/serverless, over HTTP first: each statement is one fetch, and a split write is one request Neon runs as a transaction, so it runs wherever fetch does and holds no connection between queries. It takes Neon’s ClientConfig:
import { NeonQuerierPool } from 'uql-orm/neon';import './entities.js';
export const pool = new NeonQuerierPool({ connectionString: process.env.DATABASE_URL, connectionTimeoutMillis: 5_000,});A transaction, a stream and a migration need a session, so for those a querier opens a WebSocket of its own and its release closes it. The pool holds no connection, so one at module scope serves every request, on Cloudflare Workers too, where a socket cannot outlive its request. It uses the runtime’s global WebSocket, which Node 22 and newer have. On a long-running server, prefer PgQuerierPool over TCP, which reuses its connections. Neon’s HTTP endpoint binds at most 32,767 values a statement, and UQL splits a write within that. A Neon compute scaled to zero wakes on the first query, so set the connection timeout here too.
Each HTTP request is a session of its own, so a SET lasts for that statement alone. Put a setting for one unit of work in a transaction as SET LOCAL, and one for every query on the role or database, which each new session starts with: ALTER ROLE app SET search_path = app, public.
Cloudflare Workers and Vercel Edge
Section titled “Cloudflare Workers and Vercel Edge”These run isolates, not processes, and a socket opened for one request cannot serve the next; Workers throws if you try. Turn on nodejs_compat, which transactions and withContext need. A pool holding sockets, such as PgQuerierPool on Hyperdrive, is built inside the handler; Neon, D1 and Turso hold none, so theirs stays at module scope:
import { PgQuerierPool } from 'uql-orm/postgres';import { Invoice } from './entities.js';
export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { const pool = new PgQuerierPool({ connectionString: env.HYPERDRIVE.connectionString, max: 5, }); try { return Response.json( await pool.findMany(Invoice, { $where: { paid: false }, $limit: 20 }), ); } finally { ctx.waitUntil(pool.end()); } },};ctx.waitUntil closes the pool without delaying the response. The HTTP transport takes its pool as an option, or a function that picks one per request, so a database per tenant needs nothing special.
Cold starts
Section titled “Cold starts”To keep module loading small, import the entry point you use (uql-orm/postgres, not a barrel that pulls every dialect) and register only the entities the function needs.