pg vs postgres.js vs @neondatabase/serverless: PostgreSQL Drivers for Node.js (2026)
TL;DR
postgres.js is the modern default for PostgreSQL in Node.js — faster than pg, built-in connection pooling, TypeScript-native, and a cleaner tagged template literal API. pg (node-postgres) is the battle-tested legacy choice, still heavily used via Prisma, Knex, and other ORMs. @neondatabase/serverless is the specialized choice when running in serverless/edge environments (Vercel Edge, Cloudflare Workers) where you can't use TCP connections — it tunnels queries over HTTP or WebSocket.
Key Takeaways
- pg: ~9M weekly downloads — the original, used by most ORMs under the hood (Prisma, Knex, Sequelize)
- postgres: ~2M weekly downloads — faster, cleaner API, TypeScript-native, recommended for greenfield projects
- @neondatabase/serverless: ~800K weekly downloads — HTTP/WebSocket tunnel to Postgres, required for edge runtimes
postgres.jsis 2-3x faster thanpgin benchmarks, with lower memory usage- Use
pgif you need maximum ecosystem compatibility or are using an ORM that requires it - Serverless environments (edge functions) require
@neondatabase/serverlessor a similar HTTP driver
Download Trends
| Package | Weekly Downloads | TypeScript | Pooling | Edge Runtime |
|---|---|---|---|---|
pg | ~9M | ✅ @types/pg | ✅ pg-pool | ❌ TCP only |
postgres | ~2M | ✅ Native | ✅ Built-in | ❌ TCP only |
@neondatabase/serverless | ~800K | ✅ Native | ✅ | ✅ HTTP/WS |
pg (node-postgres)
pg is the original PostgreSQL client for Node.js — battle-tested, widely used, and the driver underneath Prisma, Knex, Sequelize, and many other libraries.
Basic queries
import { Pool } from "pg"
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // Max connections in pool
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
})
// Simple query:
const { rows } = await pool.query(
"SELECT * FROM packages WHERE name = $1",
["react"]
)
// Typed result:
interface Package {
id: string
name: string
weekly_downloads: number
version: string
}
const result = await pool.query<Package>(
"SELECT id, name, weekly_downloads, version FROM packages WHERE name = $1",
["react"]
)
const pkg: Package = result.rows[0]
Transactions
const client = await pool.connect()
try {
await client.query("BEGIN")
await client.query(
"INSERT INTO packages (name, weekly_downloads) VALUES ($1, $2)",
["new-package", 0]
)
await client.query(
"UPDATE stats SET total_packages = total_packages + 1"
)
await client.query("COMMIT")
} catch (err) {
await client.query("ROLLBACK")
throw err
} finally {
client.release() // Return connection to pool
}
pg with connection pooling (PgBouncer / Supabase)
import { Pool } from "pg"
// For connection poolers (PgBouncer, Supabase Pooler):
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
// Disable prepared statements for PgBouncer transaction mode:
// These are set per-client, not pool-wide in pg
})
// Avoid prepared statements when using external poolers:
const result = await pool.query({
text: "SELECT * FROM packages WHERE name = $1",
values: ["react"],
})
Why pg is still widely used
// pg is the underlying driver for:
// - Prisma (uses @prisma/driver-adapter-pg or internal pg)
// - Knex.js: knex({ client: "pg", connection: DATABASE_URL })
// - Sequelize: dialect: "postgres"
// - Drizzle ORM: drizzle(new Pool({ connectionString }))
// - TypeORM: type: "postgres"
// If your ORM choice is Prisma/Drizzle/Knex, you don't
// interact with pg directly — it's abstracted away
postgres.js
postgres (imported as postgres) is the modern PostgreSQL client with a cleaner API, built-in connection pooling, and native TypeScript types.
Basic queries
import postgres from "postgres"
const sql = postgres(process.env.DATABASE_URL!, {
max: 20, // Max pool connections
idle_timeout: 30, // Close idle connections after 30s
connect_timeout: 10, // Connection timeout in seconds
transform: {
// Map snake_case columns to camelCase automatically:
column: postgres.camel,
},
})
// Tagged template literal syntax (safe from SQL injection):
const packages = await sql<Package[]>`
SELECT id, name, weekly_downloads, version
FROM packages
WHERE name = ${packageName}
LIMIT 10
`
// Multiple parameters:
const results = await sql<Package[]>`
SELECT * FROM packages
WHERE category = ${category}
AND weekly_downloads > ${minDownloads}
ORDER BY weekly_downloads DESC
`
Transactions
// Transaction with automatic rollback on error:
await sql.begin(async (tx) => {
const [pkg] = await tx`
INSERT INTO packages (name, weekly_downloads)
VALUES (${name}, ${downloads})
RETURNING id
`
await tx`
INSERT INTO package_history (package_id, event, created_at)
VALUES (${pkg.id}, 'created', NOW())
`
})
// Automatically commits or rolls back — no manual BEGIN/COMMIT/ROLLBACK
Typed queries (postgres.js TypeScript)
// postgres.js infers return types from generics:
interface Package {
id: string
name: string
weeklyDownloads: number // camelCase with transform.column
}
const [pkg] = await sql<Package[]>`
SELECT id, name, weekly_downloads
FROM packages WHERE name = ${name}
`
// pkg is typed as Package
// Dynamic queries (avoid SQL injection with sql() helper):
const conditions = { name: "react", category: "ui" }
const where = Object.entries(conditions).map(([k, v]) => sql`${sql(k)} = ${v}`)
const results = await sql<Package[]>`
SELECT * FROM packages
WHERE ${sql.and(...where)}
`
Array and JSON operations
// Bulk insert with postgres.js (much cleaner than pg):
const packages = [
{ name: "react", downloads: 25000000 },
{ name: "vue", downloads: 8000000 },
]
await sql`
INSERT INTO packages ${sql(packages, "name", "downloads")}
`
// Generates: INSERT INTO packages (name, downloads) VALUES ($1, $2), ($3, $4)
// JSONB columns:
const stats = { score: 92, lastChecked: new Date().toISOString() }
await sql`
UPDATE packages SET metadata = ${sql.json(stats)}
WHERE name = ${"react"}
`
// Array parameters:
const names = ["react", "vue", "angular"]
const pkgs = await sql<Package[]>`
SELECT * FROM packages WHERE name = ANY(${sql.array(names)})
`
postgres.js for serverless (with TCP)
// postgres.js supports serverless with connection reuse between requests:
import postgres from "postgres"
// Singleton pattern (reuse between Lambda/serverless invocations):
let sql: postgres.Sql | null = null
function getDb() {
if (!sql) {
sql = postgres(process.env.DATABASE_URL!, {
max: 1, // Single connection for serverless
idle_timeout: 20, // Close quickly to avoid EMFILE
fetch_types: false, // Faster startup
})
}
return sql
}
@neondatabase/serverless
@neondatabase/serverless allows PostgreSQL connections over HTTP or WebSocket — required for edge runtimes (Vercel Edge Functions, Cloudflare Workers) that can't open TCP connections.
Edge runtime setup (Vercel Edge / Cloudflare Workers)
// app/api/packages/route.ts — Vercel Edge Runtime
export const runtime = "edge"
import { neon } from "@neondatabase/serverless"
export async function GET(request: Request) {
const sql = neon(process.env.DATABASE_URL!)
const packages = await sql`
SELECT name, weekly_downloads, version
FROM packages
ORDER BY weekly_downloads DESC
LIMIT 10
`
return Response.json(packages)
}
HTTP vs WebSocket mode
// HTTP mode (neon) — one query per request, no connection state:
import { neon } from "@neondatabase/serverless"
const sql = neon(process.env.DATABASE_URL!)
// Each query = one HTTP POST to Neon's HTTP endpoint
const packages = await sql`SELECT * FROM packages LIMIT 10`
// WebSocket mode (neonConfig + Pool) — multiple queries, transactions:
import { Pool, neonConfig } from "@neondatabase/serverless"
import ws from "ws"
// Required for Node.js (not needed in edge runtimes):
neonConfig.webSocketConstructor = ws
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
// Pool API is pg-compatible — drop-in replacement:
const { rows } = await pool.query(
"SELECT * FROM packages WHERE name = $1",
["react"]
)
// Transactions require WebSocket mode:
const client = await pool.connect()
await client.query("BEGIN")
// ...
await client.query("COMMIT")
client.release()
Drizzle ORM with @neondatabase/serverless
// Edge-compatible Drizzle setup:
import { drizzle } from "drizzle-orm/neon-http"
import { neon } from "@neondatabase/serverless"
import * as schema from "./schema"
const sql = neon(process.env.DATABASE_URL!)
const db = drizzle(sql, { schema })
// Now use Drizzle ORM normally — works in edge runtime:
const packages = await db
.select()
.from(schema.packages)
.where(eq(schema.packages.category, "ui"))
.orderBy(desc(schema.packages.weeklyDownloads))
.limit(10)
Works with any Neon-compatible PostgreSQL
// @neondatabase/serverless works with:
// 1. Neon (neon.tech) — designed for it
// 2. Supabase — use Supabase's connection string
// 3. Any PostgreSQL via HTTP proxy
// For non-edge environments, prefer pg or postgres.js —
// @neondatabase/serverless adds latency due to HTTP overhead
Feature Comparison
| Feature | pg | postgres.js | @neondatabase/serverless |
|---|---|---|---|
| Weekly downloads | ~9M | ~2M | ~800K |
| TypeScript | ✅ @types/pg | ✅ Native | ✅ Native |
| Tagged template API | ❌ | ✅ | ✅ (neon mode) |
| Built-in pooling | ✅ pg-pool | ✅ Native | ✅ |
| Transactions | ✅ | ✅ Auto rollback | ✅ WS mode |
| Edge runtimes | ❌ TCP only | ❌ TCP only | ✅ HTTP/WS |
| Auto camelCase | ❌ | ✅ transform | ❌ |
| Bulk insert | Verbose | ✅ sql() helper | ✅ |
| Prepared statements | ✅ | ✅ | ❌ HTTP mode |
| pg API compatible | ✅ | ❌ | ✅ Pool mode |
| Performance | Baseline | 2-3x faster | ~10ms overhead |
When to Use Each
Choose postgres.js if:
- Starting a new project with direct database access (no ORM)
- TypeScript is a priority — native types, no @types package needed
- You want auto-camelCase column mapping
- Performance matters — 2-3x faster than pg in benchmarks
Choose pg if:
- Your ORM uses pg under the hood (Knex, Sequelize, Drizzle with pg adapter)
- You're working on a legacy codebase already using pg
- You need the widest ecosystem compatibility
- You're using PgBouncer or a connection pooler that has pg-specific considerations
Choose @neondatabase/serverless if:
- Running in Vercel Edge Functions, Cloudflare Workers, or any edge runtime
- Using Neon.tech as your hosted Postgres provider
- You need HTTP/WebSocket PostgreSQL access (TCP not available)
- Using Drizzle with
drizzle-orm/neon-httpadapter
Methodology
Download data from npm registry (weekly average, February 2026). Performance benchmarks from postgres.js repository. Feature comparison based on pg v8.x, postgres.js v3.x, and @neondatabase/serverless v0.9.x.