Why PostgreJS?
PostgreJS is a PostgreSQL driver for Node.js and Bun built from the wire protocol up - no libpq, no native bindings,
just TypeScript talking directly to PostgreSQL. That from-scratch design is also what makes it fast and light: every
byte on the wire is handled by code written for exactly that purpose, with a binary-first protocol, shared buffers,
and row/column decoding pipelines built to avoid unnecessary allocation, instead of generic string plumbing bolted
onto a client meant for text.
⚡ Blazing Fast
The numbers back it up. In PostgreJS's own benchmark suite - run head-to-head against
pg (node-postgres) and postgres
(postgres.js) on identical workloads - PostgreJS opens a connection up to 3x faster than postgres.js, pushes
pooled queries through up to 6.5x faster than pg, and fetches large result sets nearly 5x faster than pg
too. It's also the only one of the three Node drivers with a complete binary wire protocol across every data
type, rather than falling back to text for most of them. See doc/BENCHMARKS.md for the full
methodology and every scenario.
The same suite runs under Bun, where Bun's own built-in Bun.sql joins the comparison as a fourth client - see
doc/BENCHMARKS-bun.md.
🪶 Small Footprint
The same benchmarks show it using a fraction of the memory: peak heap usage typically runs 3-7x lower than both
pg and postgres.js - as much as 7x lower when streaming cursors or fetching large arrays - and it spends a
fraction of the time either of them does in garbage collection. Shared buffers and decode paths that read values
straight out of the wire buffer leave far less garbage behind per row, so there's less for the GC to clean up in
the first place.
🔋 Batteries Included
Speed and memory aside, PostgreJS is also the most complete driver of the three: a dynamic sql tag for
composable, parameterized SQL, per-query type mapping, and TC39 Explicit Resource Management (using) support are
unique to it, alongside a feature set most drivers spread across several add-on packages - connection pooling,
prepared statements, server-side cursors, LISTEN/NOTIFY, bulk COPY streams, logical replication, large objects,
two-phase commit, multi-host failover, and SCRAM channel binding - all in the one package, written in
strictly-typed TypeScript from the ground up.
🔌 Bring Your Own ORM
PostgreJS is a driver, not a framework, so what you build on top of it stays your choice:
Each adapter hands the compiled SQL straight to this client, with no pg left anywhere in the chain, and each is
compared query by query against the driver it replaces - and, where the project ships a suite of its own, run
against that too. So adopting one is a change to how the connection is created and nothing else.
🌍 Runs Where You Run
Node and Bun run the same test suite in CI on every push - Node across 20, 22, 24 and 26 against PostgreSQL 12,
16 and 18, Bun against 18 - so neither is a target that merely ought to work. Workers is not in that matrix yet: it
was measured by hand against PostgreSQL 18, a plain connection and a TLS one, and what the runtime does and does not
allow is written down in doc/CLOUDFLARE-WORKERS.md.
Installation
$ npm install postgrejs --save
Documentation
Please read 🔸 DOCUMENTATION 🔸 for detailed usage
Upgrading from 3.5? See doc/MIGRATION-v3.5-to-v3.6.md - query() no longer stops at
100 rows, and fetchAsString now returns the server's own text.
Library Overview
- Language: Pure JavaScript, with no native/binary dependencies to compile or ship.
- Strictly typed: Written entirely in TypeScript, with types shipped alongside the package.
- Modern module format: Ships as ESM; Node 20.19+/22.12+ can
require() it from CommonJS code as well.
- Promise-based API: Every asynchronous operation returns a promise - no callbacks to wrangle.
- Rigorously tested: A test suite covering the wire protocol, every data type, and connection-handling edge case,
run on every push against PostgreSQL 12 through 18.
Feature Comparison
How PostgreJS compares to pg (node-postgres) and
postgres (postgres.js). Every row was checked against the libraries' own
source rather than their documentation — versions compared: PostgreJS 3.4.0, pg 8.23.0, postgres.js 3.4.9. ✅ built
in · 🟡 partial or needs a separate package · ❌ not supported.
| Feature | PostgreJS | pg | postgres.js |
|---|
| Packaging | | | |
| Packages to install | 1 | 4 1 | 1 |
| Module system | ESM | ESM/CJS | ESM/CJS |
| Language | TS | JS 2 | JS 3 |
| Cloudflare Workers | ✅ 28 | 🟡 29 | ✅ 30 |
| Wire protocol | | | |
| Protocol version | 3.2 | 3.0 | 3.0 |
| Simple Query protocol | ✅ | ✅ | ✅ |
| Extended Query protocol | ✅ | ✅ | ✅ |
| Text wire format | ✅ | ✅ | ✅ |
| Binary wire format | ✅ | 🟡 4 | ❌ 5 |
| Per-column format selection | ✅ | ❌ | ❌ |
| Long cancel key (opt-in) | ✅ | ❌ | ❌ |
| Legacy Function Call protocol | ✅ | ❌ | ❌ |
| Graceful protocol renegotiation | ✅ |
- 1 What it takes to reach the feature set above. PostgreJS and postgres.js ship everything in the one
package you import;
pg needs pg-cursor for cursors, pg-query-stream
for row streams and pg-copy-streams for COPY, each installed and versioned separately.
- 2 Types come from the separate
@types/pg; only the pg-protocol and
pg-connection-string sub-packages are written in TypeScript.
- 3 Ships a hand-maintained
.d.ts.
- 4 Results only - parameters are always stringified. Opt-in per query or per client, and all columns at
once. No binary parser is registered for
bytea, and binary arrays decode only int4, int8 and text elements.
- 5 Both format-code counts are hardcoded to zero and parameters are stringified, so everything on the wire
is text.
- 6 Global or per-client (pg) and per-instance (postgres.js), but not per query.
- 7 Connection-level timeouts only.
- 8 Restored by calling
Error.captureStackTrace from the promise's own rejection handler, where the
synchronous stack is already gone - the caller's frames come from Node's async stack traces, so they survive an
awaited chain but not a .then()/.catch() one, and the callback API gets none at all.
- 9 Captured at the tagged template, four frames deep;
sql.unsafe() gets none.
- 10 A
types array on the query config does reach the Parse message, but the same field doubles as the
result parser override, so any row-returning query throws inside pg's own handler. Verified usable only for statements
that return no rows (pg 8.23.0).
- 11 Core has the row-limit primitive; the cursor and stream APIs are separate packages.
- 12 The core
Query refuses COPY IN; pg-copy-streams is required.
- 13
sql.prepare(name) runs PREPARE TRANSACTION inside begin, but there is no helper for the other
half - COMMIT PREPARED / ROLLBACK PREPARED have to be written as raw SQL.
- 14
end() destroys the socket when a query is still in flight, so the query is aborted rather than
awaited; only a client in pipeline mode waits for drain first.
- 15 On the client only - the pool does not forward notifications.
- 16 pg has no per-OID text encoders: a parameter is converted by its
JavaScript type rather than by the type it is going into, so there is no
count to give.
- 17 Plus every array type, whose OIDs are read from the catalog when a
connection opens rather than registered ahead of time.
Benchmarks
PostgreJS implements the full PostgreSQL wire protocol from scratch, with no dependency on pg/libpq.
doc/BENCHMARKS.md compares it against pg (node-postgres) and postgres (postgres.js)
across connection setup, simple/prepared queries, mixed-type decoding, bulk fetches, cursor streaming and pool
concurrency, each library run through its own idiomatic fast path. The numbers there are reproducible on your own
machine via pnpm run bench against the repo's own
docker/docker-compose.yml Postgres instance; see benchmark/README.md for details.
The same suite runs under Bun via pnpm run bench:bun, where it also measures Bun's own built-in Bun.sql client as
a fourth library. Those results live in doc/BENCHMARKS-bun.md and are deliberately kept
in a separate report rather than merged into the Node one: GC instrumentation, cursor streaming and binary-format
control all differ enough between the two runtimes that a single table would conflate a library difference with a
runtime difference.
Both reports list libraries in a fixed order rather than fastest-first, and mark every result statistically tied
with the leader rather than bolding a lone winner - several scenarios separate the leading drivers by about a
percent, which is less than the run-to-run spread of the measurement itself.
Support
You can report bugs and discuss features on the GitHub issues page When
you open an issue please provide version of NodeJS and PostgreSQL server.
Runtime Compatibility
- Node.js >= 20.x
- Bun - the same test suite runs under Bun in CI against PostgreSQL 18 on every push, so Bun is a supported target
rather than an untested coincidence. (Coverage is collected on the Node matrix only:
c8 instruments V8's
coverage APIs, which JavaScriptCore doesn't have.)
- Cloudflare Workers - with
nodejs_compat, and without TLS: the runtime has no way to trust a server
certificate, so a database behind TLS is reached through Hyperdrive instead. See
doc/CLOUDFLARE-WORKERS.md.
License
PostgreJS is available under the BSD 3-Clause license.