wire-protocol
PostgreSQL's frontend/backend wire protocol — the byte-level message format between client (libpq or any driver) and backend, plus the backend-side send/receive machinery. Covers `src/backend/libpq/pqcomm.c` (byte-stream framing) + `pqformat.c` (typed message building/parsing) + the message-dispatch loop in `tcop/postgres.c`. Loads when the user asks about protocol version 3.0/3.1, message types (Startup / Query / Bind / Execute / RowDescription / DataRow / CommandComplete / ReadyForQuery / ErrorResponse / etc.), simple vs extended query protocol, protocol negotiation, `pq_beginmessage` / `pq_sendstring` / `pq_endmessage` conventions, adding a new protocol message (has scenario `add-new-protocol-message`), or debugging "backend closed connection unexpectedly" wire-level. Skip when the ask is about libpq's client-side implementation (`src/interfaces/libpq/`), about the replication sub-protocol (walsender's variant), or about authentication protocol details (see `libpq-backend` subsystem).
匯入這個 Skill
選擇你的 coding agent,複製專案級或個人級安裝指令。
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wire-protocol -a codex -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wire-protocol -a codex -g -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wire-protocol -a claude-code -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wire-protocol -a claude-code -g -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wire-protocol -a github-copilot -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wire-protocol -a github-copilot -g -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wire-protocol -a cursor -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wire-protocol -a cursor -g -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wire-protocol -a gemini-cli -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wire-protocol -a gemini-cli -g -ygemini skills install https://github.com/matejformanek/postgres-claude.git --scope workspace --path .claude/skills/wire-protocolSkill 指令
在 GitHub 查看原始檔案 ↗# wire-protocol — the fe/be byte-level protocol
Every PG client-server exchange follows a strict wire protocol: length-prefixed typed messages over a socket. Understanding the message boundary + startup dance + simple-vs-extended dispatch is required for any code that touches network communication or extends the protocol.
Since PG 18, there's also **protocol 3.2** with parameter status change extensions.
## The file map
| File | KB | Role |
|---|---:|---|
| `libpq/pqcomm.c` | 30 | Low-level byte-stream framing. `PqCommMethod` vtable for socket + shared-mem front ends. Length-prefixed reads/writes. |
| `libpq/pqformat.c` | 15 | Typed message building (`pq_beginmessage` / `pq_send*` / `pq_endmessage`) and parsing (`pq_getmsg*`). The extension seam most patches touch. |
| `libpq/be-secure*.c` | — | TLS + GSS setup — sits between socket and pqcomm framing. |
| `libpq/auth*.c` | — | Auth message exchange — the startup phase before the query loop. |
| `tcop/postgres.c` | 146 | The **query loop** — reads next command message, dispatches based on message type, produces response. Sections: simple query, extended query (Parse/Bind/Execute), function call, replication protocol subset. |
| `tcop/backend_startup.c` | 34 | Startup message parsing + protocol negotiation + initial auth-handshake driver. |
The protocol itself is described in `doc/src/sgml/protocol.sgml` — read that for the definitive message list.
## The 3 message-writing patterns
### 1. `pq_beginmessage` / `pq_send*` / `pq_endmessage`
The **standard pattern** for backend → client messages. Batched into a StringInfo, sent atomically.
```c
pq_beginmessage(&buf, 'T'); // RowDescription message
pq_sendint16(&buf, numattrs);
for (i = 0; i < numattrs; i++) {
pq_sendstring(&buf, attname);
pq_sendint32(&buf, tableoid);
/* ... */
}
pq_endmessage(&buf);
```
`pq_beginmessage` sets the message-type byte; `pq_endmessage` writes the length prefix + flushes to the output buffer. Errors between them leak the buffer — see `pgstat_progress_leak` planning doc for exactly this pattern going wrong upstream.
### 2. `pq_beginmessage_reuse` / `pq_endmessage_reuse`
Same shape but reuses the caller's StringInfo across many messages (avoiding repeated palloc/pfree). Used in high-volume paths like DataRow serialization for large query results.
### 3. Direct socket write (rare)
`pq_putbytes` / `pq_flush` — bypasses the message-formatting layer. Used for the initial startup handshake and TLS negotiation.
## Simple vs Extended Query Protocol
### Simple Query ('Q' message)
Client → 'Q' + SQL text. Backend parses + plans + executes + sends RowDescription + DataRows + CommandComplete + ReadyForQuery. Simple, no parameters, no explicit binary format.
### Extended Query (Parse/Bind/Describe/Execute)
The parameterized flow:
1. Client → 'P' (Parse) + prepared-statement name + SQL text + parameter type OIDs. Backend parses + stores as unnamed or named statement.
2. Client → 'B' (Bind) + portal name + statement name + parameter values (binary or text) + result-format codes. Backend plans + creates portal.
3. Client → 'D' (Describe) — optional, requests RowDescription now.
4. Client → 'E' (Execute) + portal name + max-rows. Backend runs; sends DataRows + PortalSuspended (if max hit) or CommandComplete.
5. Client → 'S' (Sync). Backend sends ReadyForQuery. This ends the "extended query pipeline".
The extended protocol is the basis for prepared statements + parameter binding + binary format. Every serious driver uses it.
Between Parse+Bind+Execute the backend is IN a "in the middle of an extended query" state. Errors during this window are handled specially: subsequent messages ignored until Sync arrives. This is the "backend closed connection unexpectedly" investigator's clue.
## Startup phase (before query loop)
1. Client → StartupMessage (no message-type byte for backward compat) + protocol version + database/user/options.
2. Backend picks protocol version; sends AuthenticationRequest.
3. Auth exchange (SCRAM, MD5, TRUST, etc.).
4. Backend sends AuthenticationOk + BackendKeyData (for cancel).
5. Backend sends ParameterStatus for each GUC the client should know (server_version, client_encoding, etc.).
6. Backend sends ReadyForQuery. Now in the query loop.
If the client requests protocol 3.2 (PG 18+) it can subscribe to ParameterStatus updates for GUCs the server didn't originally advertise — see `_pq_.report` startup option.
## The pq_beginmessage lifecycle (canonical patch pitfall)
Every `pq_beginmessage` MUST be paired with `pq_endmessage` (or the reuse variant). Missing / duplicate calls between them → leaked StringInfo → memory pressure over time.
This is the class of bug `planning/pgstat_progress_leak/` hunted (byte-identical fix to upstream b20c952ce70): a repeated `initStringInfo` reset inside a wire message being built inside `pgstat_progress_parallel_incr_param()`. See that planning doc + notes.md for the exact pattern.
## Common patch shapes
### Add a new protocol message
Scenario exists: `knowledge/scenarios/add-new-protocol-message.md`. Short:
- Pick a byte for the message type (all-caps letters are backend→client; digits/lowercase for client→backend). Avoid collision with existing types.
- Version-gate via `PG_PROTOCOL_MAJOR / MINOR` in `libpq/pqcomm.h`.
- Client-side: driver support required. libpq changes in `src/interfaces/libpq/`.
- Docs: `doc/src/sgml/protocol.sgml`.
### Add a parameter status report
If a new GUC should be visible in every client's startup ParameterStatus:
- Add to `report_guc_option` in `guc.c`.
- Effect on clients that don't request it: none (they ignore unknown ParameterStatus messages).
### Change binary send/recv format for a type
Every type has `typsend` and `typreceive` funcs in pg_type. Change requires:
- Update the C send/recv fn (typically in `utils/adt/<type>_ops.c`).
- Bump the type's `typreceive` version guard if binary format is versioned.
- Clients using the binary format break. Coordinate on hackers-list.
### Debug "backend closed connection unexpectedly"
- Check server log for the last message BEFORE the crash — often a `LOG: could not send data to client`.
- Look for missing `pq_endmessage` or double `pq_beginmessage`.
- Wireshark filter `tcp.port == 5432` — see the last bytes actually sent.
- Try the query in simple mode (`?options=-c%20standard_conforming_strings=on` or just via psql `-c`) vs extended (via prepared statement) — narrows if issue is in extended-only path.
## Pitfalls
- **StringInfo reset inside a message = bug** — as `pgstat_progress_leak` demonstrated, `initStringInfo` inside `pq_beginmessage`/`pq_endmessage` leaks the outer buffer.
- **Message-type byte case matters** — uppercase = backend→client, lowercase = client→backend, by convention. Digit-prefixed = replication sub-protocol. Don't rebind.
- **Length includes itself but not the type byte** — the 4-byte length prefix counts itself + payload. Off-by-4 bugs are classic.
- **Extended query stays in error state until Sync** — a bug that logs "backend closed connection" during Bind may actually be an unhandled error mid-extended-query. Look for the previous message.
- **Binary format is machine-order** — network byte order for ints, but strings are UTF-8 (server encoding). Getting endianness wrong on ints = subtle data corruption.
- **Cancel is out-of-band** — cancel uses a SEPARATE connection with BackendKeyData. Not a wire message on the query connection.
- **`pq_flush` vs `pq_flush_if_writable`** — flush is blocking; flush_if_writable is non-blocking. Backends normally use non-blocking after each message.
- **TLS renegotiation is disabled** — PG doesn't support mid-connection TLS renegotiation. Don't add features assuming it works.
- **Startup message has NO type byte** — the first message from a client has a length + version + key-value payload, no type prefix. This is why the parser is separate from the query-loop parser.
## Related corpus
- **Idiom**: `error-handling` (`ereport` + protocol ErrorResponse interaction), `plan-cache` (extended-protocol prepared statement caching).
- **Subsystem**: `libpq-backend` (broader — includes auth + TLS + protocol), `tcop` (the query loop).
- **Data structures**: `stringinfo` (message buffer), `pgproc-fields` (per-backend socket handle).
- **Scenarios**: `add-new-protocol-message` (the DDL/patch guide for a new message).
- **Related planning**: `planning/pgstat_progress_leak/` — real-world buffer-lifecycle bug fixed via wire-protocol-adjacent code. Read comparison.md for the byte-identical-to-upstream analysis.
## Corpus-chain shortcut
```
python3 scripts/corpus-chain.py --scenario add-new-protocol-message
python3 scripts/corpus-chain.py --file src/backend/libpq/pqformat.c
python3 scripts/corpus-chain.py --file src/backend/tcop/postgres.c
```
## Boundary
**Use this skill** for the backend-side fe/be protocol byte-formatting layer.
**Don't use** for:
- **libpq client-side** (`src/interfaces/libpq/`) — sibling implementation with its own file map.
- **Walsender's replication sub-protocol** — different message set + different dispatch. See `logical-replication` skill for logical decoding.
- **JDBC / drivers** — external implementations of the same protocol.
- **Authentication protocols** (SCRAM / GSS / LDAP) — see `libpq-backend` subsystem.