<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/pg-vs-postgres-js-vs-neon-serverless-postgresql-drivers-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/pg-vs-postgres-js-vs-neon-serverless-postgresql-drivers-2026/raw.md -->
<!-- Source path: content/guides/pg-vs-postgres-js-vs-neon-serverless-postgresql-drivers-2026.mdx -->

---
og_image: "/images/guides/pg-vs-postgres-js-vs-neon-serverless-postgresql-drivers-2026.webp"
title: "pg vs postgres.js vs @neondatabase/serverless 2026"
description: "Compare pg (node-postgres), postgres.js, and @neondatabase/serverless for connecting to PostgreSQL in Node.js. Performance, connection pooling, TypeScript."
date: "2026-03-09"
author: "PkgPulse Team"
tags: ["nodejs", "postgresql", "typescript", "database"]
---

## 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

---

## 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](https://node-postgres.com) 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

```typescript
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

```typescript
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)

```typescript
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

```typescript
// 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](https://github.com/porsager/postgres) (imported as `postgres`) is the modern PostgreSQL client with a cleaner API, built-in connection pooling, and native TypeScript types.

### Basic queries

```typescript
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

```typescript
// 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)

```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

```typescript
// 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)

```typescript
// 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](https://github.com/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)

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// @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-http` adapter

---

## Connection Pooling in Production

Connection pooling is one of the most important operational decisions for PostgreSQL at scale. PostgreSQL has a fixed connection limit (default 100 on most hosted plans), and each connection consumes server memory — typically 5-10MB per idle connection, more under load. A Node.js application under traffic can exhaust the connection limit quickly if each serverless function invocation opens its own connection without pooling. All three drivers support connection pooling differently. `pg` uses `pg-pool`, which is mature and well-understood but requires manual configuration of pool size, idle timeout, and connection timeout. postgres.js has built-in pooling with sensible defaults — the `max` option controls pool size, and connections are automatically closed after `idle_timeout` seconds. For serverless deployments, @neondatabase/serverless is unique in that it can operate over HTTP (the `neon()` function), which is effectively connection-less — each query opens and closes a connection at the HTTP level, making it suitable for environments where persistent TCP connections are impossible. For high-traffic traditional servers, pairing `pg` or postgres.js with PgBouncer in transaction mode (where connections are returned to the pool between queries rather than between sessions) allows 10-20x higher concurrency than direct connections.

## Handling Edge Runtime Constraints

The shift toward edge runtimes — Vercel Edge Functions, Cloudflare Workers, Deno Deploy — has created a new class of database connectivity problems. These runtimes run in V8 isolates without TCP socket access, which rules out traditional PostgreSQL drivers that speak the wire protocol over a persistent TCP connection. @neondatabase/serverless solves this specifically for Neon and compatible PostgreSQL databases by tunneling queries over HTTP or WebSocket, both of which are available in edge runtimes. The HTTP mode (one query per HTTP POST) is the simplest but has the highest latency overhead — roughly 5-15ms per query for round-trip to Neon's HTTP endpoint, on top of the underlying PostgreSQL query time. The WebSocket mode allows a persistent connection over the WebSocket protocol, which edge runtimes support, enabling transactions and prepared statement reuse. For Cloudflare Workers specifically, Hyperdrive (Cloudflare's connection pooler for databases) can proxy TCP connections from Workers to any PostgreSQL database, meaning standard `pg` or postgres.js can work in Workers when Hyperdrive is configured as the connection target.

## Security Best Practices for Database Connections

Database connection strings are one of the most sensitive secrets in any application. A leaked `DATABASE_URL` with write access gives an attacker full control over application data. The best practices apply regardless of which driver you use: store connection strings in environment variables (never in source code), use platform secret managers (Vercel Environment Variables, Railway secrets, AWS Secrets Manager) rather than `.env` files committed to repositories, and prefer connection strings with least-privilege credentials — read-only database users for read-only API endpoints, restricted schemas for application users rather than superuser access. Beyond credential management, enable SSL in all non-localhost connections: `pg` supports `ssl: { rejectUnauthorized: false }` for self-signed certificates or `ssl: 'require'` for production. postgres.js accepts `ssl: 'require'` in the connection options. @neondatabase/serverless connections to Neon are always SSL by default since they go over HTTPS. For applications handling personally identifiable information, also consider enabling row-level security in PostgreSQL itself — this is independent of the driver and prevents application-level bugs from exposing data across tenant boundaries.

## TypeScript Type Safety End-to-End

Raw SQL drivers sit below the ORM layer and therefore require manual type annotation for query results. postgres.js's generic approach — `sql<Package[]>`\`SELECT...\`` — lets you annotate return types at the call site, which TypeScript then propagates to all subsequent usage. The type safety is as strong as your annotations are accurate: postgres.js cannot verify that your schema matches your TypeScript interface. pg's `pool.query<Package>()` generic works similarly. This is where type-safe query builders like Kysely (which generates TypeScript types from your schema) or ORMs like Drizzle (schema-first, generates types from the table definitions) add value — they eliminate the gap between your database schema and your TypeScript types. For teams comfortable writing raw SQL but wanting end-to-end type safety, the combination of postgres.js plus pgtyped (which parses SQL files and generates TypeScript types from them by introspecting the live database) is increasingly popular in 2026.

## ORM Compatibility and Ecosystem Integration

The choice of PostgreSQL driver is often not made directly — it is inherited from the ORM or query builder you select. Prisma abstracts the driver entirely; its internal connection management uses `pg` under the hood but exposes none of the driver API. Drizzle ORM explicitly supports both `pg` (via `drizzle-orm/node-postgres`) and postgres.js (via `drizzle-orm/postgres-js`) and `@neondatabase/serverless` (via `drizzle-orm/neon-http` and `drizzle-orm/neon-serverless`), making driver selection a config-time decision rather than an architecture commitment. Knex.js is tightly coupled to `pg` — switching to postgres.js with Knex is not supported. For teams starting a new project and planning to use a query builder or ORM, the driver choice should follow the ORM recommendation: use whatever the ORM's docs specify as the primary driver for your environment. Fighting the ORM's preferred driver adds complexity without benefit. Only teams writing raw SQL or using Drizzle's explicit driver selection have a meaningful choice between pg and postgres.js in practice. The broader takeaway is that for greenfield projects with TypeScript and Drizzle, postgres.js is the superior raw driver and pairs cleanly with the `drizzle-orm/postgres-js` adapter. For existing projects using Prisma, Sequelize, or Knex, the driver is already determined and no migration is warranted.

## Monitoring and Query Performance Observability

Production PostgreSQL applications need query performance observability beyond what the driver provides. All three drivers expose query execution time through timing wrappers or middleware patterns, but the actual query plan analysis happens at the PostgreSQL level via `EXPLAIN ANALYZE`. For identifying slow queries in production, enabling PostgreSQL's `pg_stat_statements` extension surfaces cumulative statistics on every query type executed, which is the most reliable way to find queries that need indexes. Monitoring tools like PgHero (a Rails gem that works with any PostgreSQL) and Supabase's built-in query stats surface this data with a UI. For distributed tracing that connects HTTP requests to database queries, OpenTelemetry has instrumentation packages for both `pg` (`@opentelemetry/instrumentation-pg`) and postgres.js (community packages). The @neondatabase/serverless driver benefits from Neon's built-in query insights in the Neon console, which shows slow queries and connection pool utilization without requiring additional instrumentation on the application side.

## Migration Paths and Adoption Considerations

Teams evaluating a switch between these drivers should understand the relative migration cost. Moving from pg to postgres.js is straightforward for code that uses raw queries, but requires rewriting the query syntax from parameterized placeholders to tagged template literals, and updating transaction management from explicit BEGIN/COMMIT/ROLLBACK calls to the cleaner auto-rollback pattern. For large codebases, this migration can be done incrementally by introducing postgres.js for new query modules while keeping pg for existing ones, since both drivers can operate concurrently against the same database. Moving to @neondatabase/serverless from either pg or postgres.js is primarily driven by deployment target requirements rather than API preference — it is the correct choice when the deployment environment requires HTTP-based database access, not a performance or ergonomics optimization. The initial investment in learning postgres.js's tagged template API pays dividends in code clarity, particularly for complex queries with multiple parameters where the positional placeholder numbering in pg's dollar-sign syntax becomes difficult to maintain.

## 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 →](https://www.pkgpulse.com)*

*See also: [Prisma Pulse vs Supabase Realtime vs Debezium](/guides/prisma-pulse-vs-supabase-realtime-vs-debezium-postgres-2026) and [ioredis vs node-redis vs Upstash Redis](/guides/ioredis-vs-node-redis-vs-upstash-redis-clients-2026), [Drizzle-Kit vs Atlas vs dbmate Migrations 2026](/guides/drizzle-kit-vs-atlas-vs-dbmate-schema-migration-tools-2026).*
