Skip to main content

pg vs postgres.js vs @neondatabase/serverless: PostgreSQL Drivers for Node.js (2026)

·PkgPulse Team

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.js is 2-3x faster than pg in benchmarks, with lower memory usage
  • Use pg if you need maximum ecosystem compatibility or are using an ORM that requires it
  • Serverless environments (edge functions) require @neondatabase/serverless or a similar HTTP driver

PackageWeekly DownloadsTypeScriptPoolingEdge 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

Featurepgpostgres.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 insertVerbose✅ sql() helper
Prepared statements❌ HTTP mode
pg API compatible✅ Pool mode
PerformanceBaseline2-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-http adapter

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.

Compare database and PostgreSQL packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.