> Every UQL docs page, as Markdown: https://uql-orm.dev/llms.txt
> The same docs over MCP: https://uql-orm.dev/mcp
> Before writing UQL code, read the skill: https://uql-orm.dev/.well-known/agent-skills/uql-orm/SKILL.md

# Serverless

> Run UQL in functions that freeze and thaw: pool placement, connection limits, and per-platform lifecycle.

Source: https://uql-orm.dev/serverless

A serverless function is a normal Node process with two differences that break the usual pooling advice: it is frozen between invocations and killed without warning, and there are as many processes as there is traffic. Only the [pool](https://uql-orm.dev/pool.md) changes; entities and queries do not.

## Put the pool at module scope

Every platform reuses a warm instance for consecutive requests, and module scope is evaluated once per instance rather than once per request:

```ts title="db.ts"
import { PgQuerierPool } from 'uql-orm/postgres';
import './entities.js';

export const pool = new PgQuerierPool({
  connectionString: process.env.DATABASE_URL,
  max: 2,
  connectionTimeoutMillis: 5_000,
});
```

Building the pool inside the handler is the common mistake: it pays the TCP and TLS handshake every request and leaves the previous pool’s sockets to time out. Nothing connects until the first query, so a cold start is not charged for a pool it never uses.

`connectionTimeoutMillis` is the one setting worth adding by default. Without it, a database that is asleep, unreachable or out of connections leaves the request waiting until the platform kills it, and you are billed for every second of that wait; with it, the query throws while there is still time to return a 503.

A framework dev server is the opposite case: it re-runs the module on every hot reload, so cache the pool on `globalThis` in development (see [Next.js](https://uql-orm.dev/nextjs.md)).

## `max` is per instance, not per app

`max: 10` on a platform that scales to 200 instances asks for 2000 connections; a small Postgres accepts about 100.

What `max` should track is concurrency *inside* one instance. Lambda runs one invocation at a time per execution environment, so the only concurrency there is the one your handler starts itself: a `Promise.all` over three queries wants `max: 3`, and everything else wants `1` or `2`. The exceptions are runtimes that put several requests on one instance, such as Vercel Fluid compute, where a handful is right. Sizing that low has one trap: at `max: 1` a pool call made inside a `pool.transaction` callback [deadlocks](https://uql-orm.dev/pool.md#how-big) against the connection it is already holding.

When instance count alone can exhaust the server, put a [pooler](https://uql-orm.dev/pool.md#how-big) in front of the database instead of shrinking `max`, and run [migrations](https://uql-orm.dev/migrations.md) against the direct endpoint.

## The first query after a thaw is the one that fails

A frozen instance runs no timers, so nothing on your side observes a connection going away: `idleTimeoutMillis` cannot reap it, and the `keepAlive` that `PgQuerierPool` turns on by default sends no probes. The other end is under no such freeze. A database `idle_session_timeout`, a NAT, a load balancer or a pooler drops the socket while you are suspended, and the loss shows up as `ECONNRESET` or `Connection terminated unexpectedly` on the next invocation’s first query.

UQL [discards](https://uql-orm.dev/pool.md#when-a-connection-dies-while-idle) the dead client and the next acquire opens a fresh one. The request in flight is yours to retry: on a connection error only, and never a non-idempotent write outside a [transaction](https://uql-orm.dev/querying/transactions.md).

## AWS Lambda

Module scope survives between invocations for the life of the execution environment, so the snippet above is the whole setup.

Do not call `pool.end()` per invocation: Lambda freezes the process as soon as the handler’s promise settles, so it would not finish. There is no shutdown hook worth wiring either, since the environment is torn down without running one and the database reclaims the connections when the sockets die.

```ts
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) };
};
```

A callback-style handler needs one flag that an `async` one does not: `context.callbackWaitsForEmptyEventLoop = false`. Otherwise Lambda holds the response until the event loop empties, and a pool with an idle socket in it never empties, so the invocation hangs to its timeout.

If the function sits in a VPC to reach RDS, put RDS Proxy in that VPC too, or the instance count is the connection count.

## Vercel

On Fluid compute an instance handles several concurrent requests and then suspends. `attachDatabasePool` releases idle clients before that, keeping the connection count proportional to traffic rather than to instance count:

```ts title="db.ts"
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`](https://uql-orm.dev/pool.md#reaching-the-driver) is the underlying driver pool. The higher `max` covers the concurrent requests one instance serves. Keep the default Node.js runtime, since `pg` needs TCP.

An [Astro](https://uql-orm.dev/astro.md#caching) app on this adapter has a cheaper option above the pool: with `cacheVercel()` as its cache provider, the repeat request is answered at the edge and never reaches a function or a connection.

## Neon

`uql-orm/neon` swaps `pg` for `@neondatabase/serverless`, which carries the Postgres protocol over a WebSocket to Neon’s own proxy. That is what makes it work on runtimes where `pg` will not load at all, and it takes the same `PoolConfig`:

```ts title="db.ts"
import { NeonQuerierPool } from 'uql-orm/neon';
import './entities.js';

export const pool = new NeonQuerierPool({
  connectionString: process.env.DATABASE_URL,
  max: 2,
});
```

Everything above the pool is unchanged: same entities, same queries, same transactions. The driver takes the runtime’s global `WebSocket`, which Node 24, UQL’s floor, already has. A Neon compute that has scaled to zero still wakes on the first query, so the timeout budget from the first section applies here more than anywhere.

## Runtimes where a connection cannot outlive the request

Cloudflare Workers, Vercel Edge and Deno Deploy hand out isolates, not processes. A socket opened while serving one request cannot be used to serve the next, and Workers enforces that: touching it from another request’s context throws. Module scope still runs once, so keep configuration and entity registration there, but build the pool inside the handler.

```ts title="src/index.ts"
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 {
      const invoices = await pool.findMany(Invoice, {
        $where: { paid: false },
        $limit: 20,
      });
      return Response.json(invoices);
    } finally {
      ctx.waitUntil(pool.end());
    }
  },
};
```

`ctx.waitUntil` rather than `await`: the client is not waiting for a socket to close, so the response should not be either. Which driver goes in there is the real decision, and [Hyperdrive](https://uql-orm.dev/postgres.md#cloudflare-hyperdrive) is only one answer: [D1](https://uql-orm.dev/cloudflare-d1.md) and [Turso](https://uql-orm.dev/turso.md) reach their database over `fetch()`, where a pool holds nothing, `end()` is a no-op and the whole question disappears.

Serving entities over the [HTTP transport](https://uql-orm.dev/http.md) works the same way here: the handler takes its `pool` as an option, and a function form picks one per request, so a Worker that serves a database per tenant needs nothing special.

## Cold starts

The first request to a new instance pays for the module graph, the first connection, and, on a database that scales to zero, the wake-up. Only the first is yours to shrink: import the entry point you use (`uql-orm/postgres`, not a barrel that pulls in every dialect) and register only the entities the function needs.
