Skip to content

Commit 2d16afe

Browse files
committed
Add pipelining documentation and pool-level integration
- New features/pipelining.mdx documenting the opt-in flag - Client and Pool API reference updated - Pool accepts `pipelining: true` and sets it on every client it creates
1 parent 2ee5bf4 commit 2d16afe

File tree

6 files changed

+180
-0
lines changed

6 files changed

+180
-0
lines changed

docs/pages/apis/client.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,18 @@ const client = new Client()
5656
await client.connect()
5757
```
5858

59+
## client.pipelining
60+
61+
`client.pipelining: boolean`
62+
63+
When set to `true`, queries are sent to the server immediately without waiting for previous queries to complete. Defaults to `false`. See [Pipelining](/features/pipelining) for details and examples.
64+
65+
```js
66+
const client = new Client()
67+
await client.connect()
68+
client.pipelining = true
69+
```
70+
5971
## client.query
6072

6173
### QueryConfig

docs/pages/apis/pool.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ type Config = {
6969
// If the function throws or returns a promise that rejects, the client is destroyed
7070
// and the error is returned to the caller requesting the connection.
7171
onConnect?: (client: Client) => void | Promise<void>
72+
73+
// When set to true, enables query pipelining on every client the pool creates.
74+
// Pipelined clients send queries to the server without waiting for previous responses.
75+
// Default is false. See /features/pipelining for details.
76+
pipelining?: boolean
7277
}
7378
```
7479

docs/pages/features/_meta.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export default {
22
connecting: 'Connecting',
33
queries: 'Queries',
4+
pipelining: 'Pipelining',
45
pooling: 'Pooling',
56
transactions: 'Transactions',
67
types: 'Data Types',

docs/pages/features/pipelining.mdx

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
---
2+
title: Pipelining
3+
---
4+
5+
import { Alert } from '/components/alert.tsx'
6+
7+
## What is pipelining?
8+
9+
By default node-postgres waits for each query to complete before sending the next one. This means every query pays a full network round-trip of latency. **Query pipelining** sends multiple queries to the server without waiting for responses, and the server processes them in order. Each query still gets its own result (or error), but you avoid the idle time between them.
10+
11+
```
12+
sequential (default) pipelined
13+
───────────────────── ─────────────────────
14+
client ──Parse──▶ server client ──Parse──▶ server
15+
client ◀──Ready── server ──Parse──▶
16+
client ──Parse──▶ server ──Parse──▶
17+
client ◀──Ready── server client ◀──Ready── server
18+
client ──Parse──▶ server client ◀──Ready── server
19+
client ◀──Ready── server client ◀──Ready── server
20+
```
21+
22+
In benchmarks, pipelining typically delivers **2-3x throughput** for batches of simple queries on a local connection, with larger gains over higher-latency links.
23+
24+
## Enabling pipelining
25+
26+
Pipelining is opt-in. Set `client.pipelining = true` after connecting:
27+
28+
```js
29+
import { Client } from 'pg'
30+
31+
const client = new Client()
32+
await client.connect()
33+
client.pipelining = true
34+
35+
const [r1, r2, r3] = await Promise.all([
36+
client.query('SELECT 1 AS num'),
37+
client.query('SELECT 2 AS num'),
38+
client.query('SELECT 3 AS num'),
39+
])
40+
41+
console.log(r1.rows[0].num, r2.rows[0].num, r3.rows[0].num) // 1 2 3
42+
43+
await client.end()
44+
```
45+
46+
All query types work with pipelining: plain text, parameterized, and named prepared statements.
47+
48+
## Pipelining with a pool
49+
50+
Pass `pipelining: true` in the pool config to enable it on every client the pool creates:
51+
52+
```js
53+
import { Pool } from 'pg'
54+
55+
const pool = new Pool({ pipelining: true })
56+
57+
const client = await pool.connect()
58+
// client.pipelining is already true
59+
60+
const [users, orders] = await Promise.all([
61+
client.query('SELECT * FROM users WHERE id = $1', [1]),
62+
client.query('SELECT * FROM orders WHERE user_id = $1', [1]),
63+
])
64+
65+
client.release()
66+
```
67+
68+
<Alert>
69+
<div>
70+
<code>pool.query()</code> checks out a client for a single query and releases it immediately, so pipelining has no effect there. Use <code>pool.connect()</code> to check out a client and send multiple queries on it.
71+
</div>
72+
</Alert>
73+
74+
## Error isolation
75+
76+
Each pipelined query gets its own error boundary. A failing query in the middle of a batch does not break the other queries:
77+
78+
```js
79+
const results = await Promise.allSettled([
80+
client.query('SELECT 1 AS num'),
81+
client.query('SELECT INVALID SYNTAX'),
82+
client.query('SELECT 3 AS num'),
83+
])
84+
85+
console.log(results[0].status) // 'fulfilled'
86+
console.log(results[1].status) // 'rejected'
87+
console.log(results[2].status) // 'fulfilled'
88+
```
89+
90+
This works because node-postgres sends a `Sync` message after each query, which is how PostgreSQL delimits error boundaries in the extended query protocol.
91+
92+
## Named prepared statements
93+
94+
Named prepared statements work with pipelining. When two pipelined queries share the same statement name, node-postgres sends `Parse` only once and reuses the prepared statement for subsequent queries:
95+
96+
```js
97+
const queries = Array.from({ length: 100 }, (_, i) => ({
98+
name: 'get-user',
99+
text: 'SELECT * FROM users WHERE id = $1',
100+
values: [i],
101+
}))
102+
103+
const results = await Promise.all(queries.map(q => client.query(q)))
104+
```
105+
106+
## Graceful shutdown
107+
108+
Calling `client.end()` while pipelined queries are in flight will wait for all of them to complete before closing the connection:
109+
110+
```js
111+
client.pipelining = true
112+
113+
const p1 = client.query('SELECT 1')
114+
const p2 = client.query('SELECT 2')
115+
const endPromise = client.end()
116+
117+
// Both queries will resolve normally
118+
const [r1, r2] = await Promise.all([p1, p2])
119+
await endPromise
120+
```
121+
122+
## When to use pipelining
123+
124+
Pipelining is most useful when you have multiple **independent** queries that don't depend on each other's results. Common use cases:
125+
126+
- Fetching data from multiple tables in parallel for a page load
127+
- Inserting or updating multiple rows simultaneously
128+
- Running a batch of analytics queries
129+
130+
<div className="alert alert-warning">
131+
Do not use pipelining inside a transaction if you need to read the result of one query before issuing the next. Pipelined queries are all sent before any responses arrive, so you cannot branch on intermediate results. For dependent queries within a transaction, use sequential <code>await</code> calls instead.
132+
</div>

packages/pg-pool/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ class Pool extends EventEmitter {
239239

240240
newClient(pendingItem) {
241241
const client = new this.Client(this.options)
242+
if (this.options.pipelining) {
243+
client.pipelining = true
244+
}
242245
this._clients.push(client)
243246
const idleListener = makeIdleListener(this, client)
244247

packages/pg-pool/test/index.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,33 @@ describe('pool', function () {
203203
})
204204
})
205205

206+
it('enables pipelining on clients when configured', async function () {
207+
const pool = new Pool({ pipelining: true })
208+
const client = await pool.connect()
209+
expect(client.pipelining).to.be(true)
210+
211+
const [r1, r2, r3] = await Promise.all([
212+
client.query('SELECT 1 AS num'),
213+
client.query('SELECT 2 AS num'),
214+
client.query('SELECT 3 AS num'),
215+
])
216+
217+
expect(r1.rows[0].num).to.eql(1)
218+
expect(r2.rows[0].num).to.eql(2)
219+
expect(r3.rows[0].num).to.eql(3)
220+
221+
client.release()
222+
return pool.end()
223+
})
224+
225+
it('does not enable pipelining by default', async function () {
226+
const pool = new Pool()
227+
const client = await pool.connect()
228+
expect(client.pipelining).to.be(false)
229+
client.release()
230+
return pool.end()
231+
})
232+
206233
it('recovers from query errors', function () {
207234
const pool = new Pool()
208235

0 commit comments

Comments
 (0)