> 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

# Pool

> Build the connection pool every query runs on, size it, and close it.

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

Every query runs on a pool, which owns the connections and lends one to each unit of work. It is the only part of UQL tied to your database: entities, queries and [migrations](https://uql-orm.dev/migrations.md) stay the same whichever pool you build.

## One per process

Build the pool once, at module scope, in the `uql.config.ts` the [`uql-migrate`](https://uql-orm.dev/migrations.md) CLI also reads, and import it everywhere:

```ts title="uql.config.ts"
import type { Config } from 'uql-orm';
import { PgQuerierPool } from 'uql-orm/postgres';
import { Post, User } from './shared/models/index.js';

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

export default { pool, entities: [User, Post] } satisfies Config;
```

The first argument is the driver’s own config, passed through as is. The second holds UQL’s options: [`logger`, `logValues` and `slowQuery`](https://uql-orm.dev/logging.md), [`namingStrategy`](https://uql-orm.dev/naming-strategy.md), a default [`schema`](https://uql-orm.dev/multiple-schemas.md) and `listeners`.

A second pool doubles the connections without adding capacity. The exceptions are dev servers that re-run modules on hot reload, which cache the pool on `globalThis` (see [Next.js](https://uql-orm.dev/nextjs.md)), and [serverless functions](https://uql-orm.dev/serverless.md).

Building a pool opens no connection, so a wrong password shows up at the first query. To fail at boot instead, run one there:

```ts
await pool.all`SELECT 1`;
```

## Which pool

| Entry point | Class | First argument |
| - | - | - |
| `uql-orm/postgres` | `PgQuerierPool` | `pg`’s `PoolConfig` |
| `uql-orm/neon` | `NeonQuerierPool` | `@neondatabase/serverless`’s `ClientConfig` |
| `uql-orm/cockroachdb` | `CrdbQuerierPool` | `pg`’s `PoolConfig` |
| `uql-orm/mysql` | `MySql2QuerierPool` | `mysql2`’s `PoolOptions` |
| `uql-orm/mariadb` | `MariadbQuerierPool` | `mariadb`’s pool config |
| `uql-orm/mssql` | `MsSqlQuerierPool` | `mssql`’s `config` |
| `uql-orm/bun-sql` | `BunSqlQuerierPool` | Bun’s `SQL.Options` |
| `uql-orm/sqlite` | `SqliteQuerierPool`, `NodeSqliteQuerierPool` | file path, then the driver’s options |
| `uql-orm/pglite` | `PgliteQuerierPool` | data directory, then PGlite’s options |
| `uql-orm/libsql` | `LibsqlQuerierPool` | `@libsql/client`’s `Config`, or a client you built |
| `uql-orm/turso` | `TursoQuerierPool` | Turso Cloud settings |
| `uql-orm/turso/local` | `TursoLocalQuerierPool` | file path, then the engine’s options |
| `uql-orm/d1` | `D1QuerierPool` | the Worker’s D1 binding, or a session from it |
| `uql-orm/mongodb` | `MongodbQuerierPool` | connection URI, then `MongoClientOptions` |

SQLite, PGlite and embedded Turso keep one connection that every querier shares: the methods are the same, but queries run one at a time and two queriers cannot hold separate transactions. D1, Turso Cloud and Neon hold no connection and talk over `fetch()`; Turso Cloud gives each querier a server session of its own, and Neon a WebSocket for a transaction or a stream.

## How big

`max` (`connectionLimit` on `mysql2` and `mariadb`, `maxPoolSize` on MongoDB) caps one process’s connections. Use the smallest of: the server’s limit (`max_connections`) divided by the processes that connect, the queries the process actually runs at once, and what the database can do in parallel (roughly its cores). An HTTP server handling 40 requests at a time wants tens; a worker taking one job at a time wants two or three.

Always set an acquire timeout (`connectionTimeoutMillis` on the `pg` family, `connectTimeout` on `mysql2`, `acquireTimeout` on `mariadb`). The `pg` family waits forever without one, so an unreachable or exhausted database hangs every request.

Pool calls inside `pool.transaction` reuse the transaction’s connection, so even `max: 1` cannot deadlock. See [Transactions](https://uql-orm.dev/querying/transactions.md).

When the processes alone can exhaust the server, put a pooler in front instead of shrinking `max`: PgBouncer or RDS Proxy, [Supavisor](https://uql-orm.dev/supabase.md) on Supabase, [Hyperdrive](https://uql-orm.dev/postgres.md#cloudflare-hyperdrive) on Workers. They run in transaction mode, so keep `SET LOCAL`, advisory locks and temp tables inside one [`transaction`](https://uql-orm.dev/querying/transactions.md), and run migrations against the direct endpoint.

## Closing it

`pool.end()` closes idle connections and waits for borrowed ones. Call it after the server stops accepting requests:

```ts
process.on('SIGTERM', async () => {
  await server.close();
  await pool.end();
});
```

[NestJS](https://uql-orm.dev/nestjs.md) does this when `app.enableShutdownHooks()` is on. Skip it in [serverless functions](https://uql-orm.dev/serverless.md): the platform freezes the process before the close finishes. On D1 and Turso Cloud it does nothing, and a [libSQL](https://uql-orm.dev/turso.md#libsql) pool leaves a client you passed in open. Once ended, the `pg`, `mysql2` and SQL Server pools refuse new connections; the single-connection pools reopen on the next query.

## When a connection dies while idle

A NAT, load balancer or `idle_session_timeout` can drop an idle connection. On the `pg`, CockroachDB, MariaDB and SQL Server pools, UQL logs the drop through the pool’s [`logger`](https://uql-orm.dev/logging.md), even with logging off, and discards the connection instead of letting the driver’s `'error'` event crash the process. The `pg` and CockroachDB pools also turn on `keepAlive` by default.

The query that was running is yours to retry, on connection errors only and never for a non-idempotent write outside a [transaction](https://uql-orm.dev/querying/transactions.md). A querier released with a transaction still open is rolled back and logged; if that rollback fails, the connection is discarded.

## More than one pool

A read replica or a database per tenant needs its own pool. Write functions against a [`UniversalQuerier`](https://uql-orm.dev/querying/querier.md#accept-a-universalquerier), which takes a pool or a querier, so the caller picks:

```ts
import type { UniversalQuerier } from 'uql-orm';

const unpaid = (db: UniversalQuerier, companyId: number) =>
  db.findMany(Invoice, { $where: { companyId, paid: false } });

await unpaid(replica, 42);
await primary.transaction((querier) => unpaid(querier, 42));
```

For tenants, build pools on demand in a capped map and `end()` the ones you evict. The [HTTP handler](https://uql-orm.dev/http.md#the-pool) can pick a pool per request. Entities are registered once and every pool serves them.

## Reaching the driver

`pool.pool` is the driver’s pool on the `pg`, CockroachDB, `mysql2`, `mariadb` and SQL Server pools, for libraries such as `attachDatabasePool` from `@vercel/functions` or `connect-pg-simple`. Bun’s pool has a `pg`-compatible shim there and its `SQL` instance at `pool.sql`; D1’s binding is `pool.db`. `pool.dialect.dialectName` names the backend (`'postgres'`, `'cockroachdb'`, `'mysql'`, `'mariadb'`, `'mssql'`, `'sqlite'` or `'mongodb'`).

## In tests

Swap in a pool with no server behind it: [PGlite](https://uql-orm.dev/pglite.md) is Postgres in WASM, and `new SqliteQuerierPool(':memory:')` gives each pool a fresh database.

```ts title="test/db.ts"
import { PgliteQuerierPool } from 'uql-orm/pglite';

export const pool = new PgliteQuerierPool();
```

Build one per suite and `end()` it afterwards, or the runner may not exit. Both hold one connection, so a test that needs its own transaction needs its own pool.
