jackc/pgx · critical
pipeline: no more results but expected ReadyForQuery
Error message
pipeline: no more results but expected ReadyForQuery
What it means
Thrown by Pipeline.Close() in pgconn/pgconn.go:2971. While draining a pipeline, the driver loops as long as ExpectedReadyForQuery() > 0, calling getResults() to consume each queued request's reply. When getResults() returns (nil, nil) it means the internal request queue is already exhausted (ExtractFrontRequestType returned pipelineNil) but the driver is still owed one or more ReadyForQuery sync markers from the server. A FATAL server-side ErrorResponse consumes queued request slots without the server ever emitting ReadyForQuery, so the protocol stream becomes unsynchronizable and the connection is forcibly closed (asyncClose).
Source
Thrown at pgconn/pgconn.go:2971
return p.err
}
for p.state.ExpectedReadyForQuery() > 0 {
results, err := p.getResults()
if err != nil {
p.err = err
var pgErr *PgError
if !errors.As(err, &pgErr) {
p.conn.asyncClose()
break
}
} else if results == nil {
// getResults returns (nil, nil) when the request queue is exhausted but
// ExpectedReadyForQuery is still > 0. This can happen when FATAL errors consume
// queued request slots without the server ever sending ReadyForQuery.
p.conn.asyncClose()
if p.err == nil {
p.err = errors.New("pipeline: no more results but expected ReadyForQuery")
}
break
}
}
p.conn.contextWatcher.Unwatch()
p.conn.unlock()
return p.err
}
// DeadlineContextWatcherHandler handles canceled contexts by setting a deadline on a net.Conn.
type DeadlineContextWatcherHandler struct {
Conn net.Conn
// DeadlineDelay is the delay to set on the deadline set on net.Conn when the context is canceled.
DeadlineDelay time.Duration
}View on GitHub (pinned to ec1a0befd2)
Solutions
- Treat the *PgConn as dead: do not reuse it. After Close() returns this error, open a fresh connection (or let pgxpool hand you a healthy one).
- Inspect the error chain with errors.As for *pgconn.PgError; its Severity/SCode (e.g. 57P01 admin_shutdown, 53200 out_of_memory, 57P02 crash_shutdown) names the real server-side cause that desynchronized the pipeline.
- Ensure every batched group of Send* calls is followed by exactly one SendPipelineSync (or Sync) so ExpectedReadyForQuery tracks the queue correctly.
- If talking through PgBouncer or another pooler, use a session-pooling mode or verify the pooler supports PostgreSQL pipelining; otherwise avoid pipeline mode for that endpoint.
- Guard the whole pipeline with a context timeout generous enough for the largest batch so the context cannot cancel mid-drain.
Example fix
// before: pipeline not synced, FATAL mid-batch desyncs state
pipe := conn.StartPipeline(ctx)
pipe.SendQueryParams("SELECT $1::int", [][]byte{{'1'}}, nil, nil, nil)
// no Sync sent; a FATAL arrives -> Close() -> "no more results but expected ReadyForQuery"
err := pipe.Close()
// after: one Sync per batch, check for *PgError, reconnect on fatal
pipe := conn.StartPipeline(ctx)
pipe.SendQueryParams("SELECT $1::int", [][]byte{{'1'}}, nil, nil, nil)
pipe.Sync()
for {
res, err := pipe.GetResults()
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) { /* handle/log SQLSTATE */ }
break
}
if res == nil { break }
}
if err := pipe.Close(); err != nil {
conn, _ = pgconn.Connect(ctx, connString) // connection is unusable, reconnect
} Defensive patterns
Strategy: try-catch
Try / catch
err := pipe.Close()
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
log.Printf("pipeline aborted by server: SQLSTATE=%s severity=%s", pgErr.Code, pgErr.Severity)
}
// Connection is dead and must be discarded; pgxpool will replace it.
_ = conn.Close(ctx)
conn, err = pgconn.Connect(ctx, connString) // reconnect or request a fresh pooled conn
} Prevention
- Call SendPipelineSync once per batched group of Send* calls so ExpectedReadyForQuery tracks the request queue.
- Drain every result via GetResults before Close so server FATAL errors surface as *PgError instead of the drain-time desync message.
- Do not pipeline through poolers/proxies that do not support PostgreSQL extended-query pipelining.
- Use a context timeout sized to the largest batch to avoid cancellation mid-drain.
- Treat any error from Pipeline.Close as fatal for that connection - never reuse the *PgConn.
When it happens
Trigger: Calling Pipeline.Close() after a FATAL-severity ErrorResponse arrived mid-pipeline (e.g. pg_terminate_backend, out-of-memory, admin shutdown, losing the TCP connection between SendQueryParams and the matching Sync). Also reproducible when SendPipelineSync was not called once per batched group, so ExpectedReadyForQuery and the request queue fall out of agreement, or when a non-PostgreSQL intermediary (some PgBouncer versions in transaction-pooling mode) mishandles extended-query pipelining.
Common situations: Server killed by an administrator or OOM killer during a pipelined batch; max_connections reached mid-pipeline; network drop or load-balancer idle timeout severing the socket while queries are queued; using pipeline mode against a pooler or proxy that does not faithfully forward extended-protocol messages; a context cancellation that aborts the pipeline mid-flight.
Related errors
- bad authentication message size
- bad authentication message size
- bad authentication message size
- authentication message too short
- no reference to batch
AI-assisted analysis of jackc/pgx@ec1a0befd2 (2026-08-04).
Data as JSON: /data/errors/ec82e20044fa23be.json.
Report an issue: GitHub.