@neondatabase/serverless
@neondatabase/serverless is Neon’s JavaScript and TypeScript PostgreSQL driver for runtimes where opening a normal TCP database socket is awkward or impossible. Its `neon()` tagged template sends one-shot queries and fixed batches over HTTPS. Its node-postgres-compatible `Pool` and `Client` use WebSockets when an application needs a session, prepared state, or an interactive transaction. The package is tuned for Neon, although another PostgreSQL server can work behind a separately operated WebSocket proxy.
The direct choice for Neon from edge and serverless runtimes, especially when most work fits one HTTP statement or a fixed batch. Choose the transport consciously: `neon()` is simpler and cheaper to manage, while Pool and Client bring session semantics plus WebSocket lifecycle work.
Use it if
- You query Neon from edge functions, workers, or short-lived serverless handlers that cannot open ordinary PostgreSQL TCP connections
- Most operations are independent SQL statements and you want a parameterized tagged template over low-latency HTTPS
- You need node-postgres-compatible Pool or Client objects for Kysely, Zapatos, sessions, or interactive transactions over WebSockets
- You want one dependency with included TypeScript declarations and both ESM and CommonJS exports
- Your production runtime is Node 18 or older: version 1.0 raised the package engine requirement to Node 19, specifically to avoid dynamic crypto imports that caused bundler problems
- You need an interactive transaction through the `neon()` HTTP API: the README says HTTP handles one query at a time, while `transaction()` only submits a predeclared array as one non-interactive transaction
- You plan to create one Pool globally in an edge or serverless process: the README says WebSocket Pool and Client instances cannot outlive a request there and must be created, used, and closed inside the handler
- You expect generated row types or an ORM: the 1.1 declarations return Record<string, any> or any[] rows, so SQL result shape is still your responsibility
- You are connecting to ordinary PostgreSQL and do not want another network component: non-Neon servers require a self-hosted WebSocket proxy, while pg or postgres can use a direct TCP connection in a normal Node service
Setup reality
There are no runtime dependencies, peer packages, native builds, or generated clients, but a working connection still starts in the Neon console: copy the full PostgreSQL URL into a server-side DATABASE_URL secret. Version 1.1 requires Node 19 or newer. For isolated statements, `neon(DATABASE_URL)` works through fetch with no WebSocket package. Since version 1, that function must be used as a tagged template; conventional `sql('...', values)` calls throw, and manually assembled SQL belongs in `sql.query(text, values)`. Sessions change the setup. Pool and Client use WebSockets, and Node 21 and earlier need a constructor such as the `ws` package assigned to `neonConfig.webSocketConstructor`. In edge handlers, create and close those objects within the same request because the socket cannot survive the request boundary. The fetch transaction helper accepts a ready array or a non-async callback that returns an array; application logic cannot pause between statements. HTTP timeouts are not a top-level option, so pass an AbortSignal through fetchOptions and clear your timer. Direct browser use is technically possible but version 1.0.1 prints a security warning because shipping a database connection string to untrusted client code is usually the wrong boundary. Connecting to non-Neon PostgreSQL adds a separately deployed WebSocket proxy and advanced transport configuration. Cloudflare Worker users should also compare Hyperdrive, which the project README explicitly recommends considering.
Patterns
Run a safe one-shot query over HTTPrun-parameterized-query
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL);
const [post] = await sql`
SELECT id, title
FROM posts
WHERE id = ${postId}
`;Interpolated values are parameterized. The HTTP path does not create a reusable database session.
Insert a row and return itinsert-returning-row
const [created] = await sql`
INSERT INTO posts (title, author_id)
VALUES (${title}, ${authorId})
RETURNING id, title, author_id
`;By default the result is the rows array itself, not a node-postgres result object.
Compose parameterized SQL fragmentscompose-query-fragments
const filter = publishedOnly
? sql`WHERE published_at IS NOT NULL AND author_id = ${authorId}`
: sql`WHERE author_id = ${authorId}`;
const rows = await sql`
SELECT id, title FROM posts ${filter} ORDER BY id DESC
`;Version 1 compiles fragments lazily, so parameter positions remain correct when fragments are nested.
Execute a query stored in a stringrun-query-string
const text = 'SELECT id, title FROM posts WHERE author_id = $1';
const rows = await sql.query(text, [authorId]);Use sql.query() for text plus numbered placeholders. Calling sql(text, values) is an error in version 1.
Insert a trusted dynamic identifieruse-dynamic-identifier
const allowed = new Set(['created_at', 'title']);
if (!allowed.has(sortColumn)) throw new Error('Invalid sort column');
const rows = await sql`
SELECT id, title FROM posts
ORDER BY ${sql.unsafe(sortColumn)}
`;Parameters cannot represent table or column names. sql.unsafe() performs no escaping, so enforce a fixed allowlist first.
Send a fixed transaction over HTTPrun-fixed-transaction
const [accountRows, auditRows] = await sql.transaction([
sql`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${fromId} RETURNING balance`,
sql`INSERT INTO audit_log (account_id, amount) VALUES (${fromId}, ${amount}) RETURNING id`,
]);The statements are submitted together as a non-interactive transaction. You cannot inspect the first result before defining the second query.
Run a serializable read-only transactionset-transaction-isolation
const [summary, totals] = await sql.transaction(
[
sql`SELECT * FROM daily_summary`,
sql`SELECT sum(amount) AS total FROM payments`,
],
{ isolationLevel: 'Serializable', readOnly: true, deferrable: true },
);deferrable is meaningful only with a serializable, read-only PostgreSQL transaction.
Get fields, command, and row countreturn-full-results
const sqlWithMeta = neon(process.env.DATABASE_URL, { fullResults: true });
const result = await sqlWithMeta`SELECT id, title FROM posts`;
console.log(result.rowCount, result.command, result.fields, result.rows);fullResults changes the return shape from a rows array to a node-postgres-style result object.
Return compact array rowsreturn-array-rows
const arraySql = neon(process.env.DATABASE_URL, { arrayMode: true });
const rows = await arraySql`SELECT id, title FROM posts ORDER BY id`;
for (const [id, title] of rows) console.log(id, title);Array mode drops column-name keys, so keep the SELECT column order explicit and stable.
Abort a slow HTTP querycancel-slow-query
const controller = new AbortController();
const timer = setTimeout(() => controller.abort('database timeout'), 5000);
try {
return await sql.query('SELECT * FROM slow_report WHERE team_id = $1', [teamId], {
fetchOptions: { signal: controller.signal },
});
} finally {
clearTimeout(timer);
}HTTP query cancellation is passed through fetchOptions. Always clear the timer after success or failure.
Use Pool for an interactive transactionrun-interactive-transaction
import { Pool } from '@neondatabase/serverless';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const client = await pool.connect();
try {
await client.query('BEGIN');
const { rows: [account] } = await client.query(
'SELECT balance FROM accounts WHERE id = $1 FOR UPDATE',
[accountId],
);
if (account.balance < amount) throw new Error('Insufficient funds');
await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, accountId]);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
await pool.end();
}Pool uses WebSockets. In a serverless handler, create and close it within that same request.
Provide WebSocket support on older Node releasesconfigure-node-websocket
import { Pool, neonConfig } from '@neondatabase/serverless';
import ws from 'ws';
neonConfig.webSocketConstructor = ws;
const pool = new Pool({ connectionString: process.env.DATABASE_URL });The README requires this on Node 21 and earlier for Pool or Client. The `neon()` HTTP API does not need WebSockets.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pg | npm | You run a long-lived Node.js service with direct TCP access and want the standard node-postgres ecosystem |
| postgres | npm | You want a compact tagged-template PostgreSQL client in a conventional Node, Bun, or Deno process |
| @electric-sql/pglite | npm | You need embedded PostgreSQL in a browser, test, or local application rather than a remote managed database |