Architecture
Wire protocol
Clients and the server talk in small typed messages over one TCP or Unix socket. Pick a scenario below to watch the exact message sequence.
In plain words
When your application talks to PostgreSQL it does not send free text. It sends small messages, each starting with one letter that says what it is: Q for a query, D for a row of data, Z for "ready for the next command", and so on.
A conversation starts with a startup message (who am I, which database), a password exchange, and some settings the server reports back. Then the client sends queries. The simple way packs the whole SQL text, values included, into one Q message. The extended way splits it into steps: Parse the SQL once, Bind values to it, Execute it. That is what "prepared statements" are, and it keeps values separate from SQL, which protects against SQL injection.
Bulk loading with COPY streams many rows in a row of data messages. And to cancel a running query, the client opens a second connection and sends a secret key it received at login, because the first connection is busy.
For DBAs and operators
Message format
Every message after start-up is type byte + int32 length + payload. The start-up packet, SSLRequest (code 80877103), GSSENCRequest (80877104) and CancelRequest (80877102) have no type byte. PostgreSQL 18 adds protocol version 3.2, whose main change is a variable-length cancel key (up to 256 bytes instead of 4; the PostgreSQL 18 server itself sends 32); libpq 18 still requests 3.0 unless you set max_protocol_version=3.2 (or latest) in the connection string.
Simple versus extended query
- Simple (
Q): one round trip, text results, may contain several statements separated by semicolons, which run in one implicit transaction. Values are inlined, so the driver must quote them. - Extended (
Parse,Bind,Describe,Execute,Sync): parameters are sent separately, binary formats are possible, one statement per Parse. Errors make the server skip messages until the nextSync, which is what makes pipeline mode (libpq 14+) safe.
Prepared statements and the plan cache
A named Parse (or SQL PREPARE) keeps the parsed statement in the backend's memory until Close or disconnect. For the first five executions the planner builds a custom plan for the actual values; after that it switches to a generic plan if that is not estimated to be more expensive. plan_cache_mode forces either. Transaction-mode poolers used to break named statements; PgBouncer 1.21+ can track them (max_prepared_statements).
SELECT name, statement, prepare_time, generic_plans, custom_plans
FROM pg_prepared_statements;Cancel requests
At login the server sends BackendKeyData (PID + secret). pg_cancel_backend() from SQL and Ctrl+C in psql both end up sending SIGINT to the backend; the protocol path opens a fresh connection; the process the postmaster forks for it checks the key and signals the backend. Cancellation is best effort: if the query already finished, nothing happens. Behind a pooler or load balancer the cancel may land on a different server, a common cause of "cancel does nothing".
Debugging
log_connectionsandlog_disconnectionson the server;PGSSLMODE,sslnegotiation=direct(PostgreSQL 17+) on the client.- libpq tracing:
PQtrace(); JDBC:loggerLevel=TRACE; or Wireshark, which decodes the protocol on port 5432. pg_stat_activity.state = 'idle in transaction'means the last message the server got was a command inside a transaction and the client went quiet.
For developers: inside the source
Where it lives
protocol.h: every message type byte as aPqMsg_*macro (PqMsg_Query'Q',PqMsg_DataRow'D',PqMsg_ReadyForQuery'Z' …).pqcomm.h:PG_PROTOCOL_LATEST(3.2),CANCEL_REQUEST_CODE,NEGOTIATE_SSL_CODE,NEGOTIATE_GSS_CODE.- Server I/O:
pqcomm.c(socket buffers,pq_getmessage(),socket_putmessage()) andpqformat.c(pq_beginmessage(),pq_sendint32(),pq_endmessage()). - Startup:
backend_startup.c(ProcessSSLStartup(),ProcessStartupPacket(),SendNegotiateProtocolVersion(),ProcessCancelRequestPacket()); authentication exchange inauth.c(sendAuthRequest()). - Client side:
fe-connect.c,fe-protocol3.c(pqParseInput3()),fe-trace.c(PQtrace()).
The backend main loop
PostgresMain() sends BackendKeyData and the first ReadyForQuery, then loops: ReadCommand() → SocketBackend() reads one message and switches on the type byte:
PqMsg_Query → exec_simple_query()
PqMsg_Parse → exec_parse_message() CachedPlanSource
PqMsg_Bind → exec_bind_message() portal + GetCachedPlan()
PqMsg_Execute → exec_execute_message()
PqMsg_Sync → finish_xact_command(); send ReadyForQueryAfter an error in extended mode the static flag ignore_till_sync makes the loop discard messages until Sync. Rows go out through a DestReceiver: printtup() in printtup.c builds DataRow; EndCommand() and ReadyForQuery() in dest.c send CommandComplete and Z.
Details worth knowing
- Plan cache:
choose_custom_plan()uses custom plans whilenum_custom_plans < 5, then compares average custom cost with the generic plan cost. - COPY:
SendCopyBegin()/CopySendEndOfRow()incopyto.c;ReceiveCopyBegin()/CopyGetData()incopyfromparse.c. - Cancel: the key (4 bytes for protocol 3.0,
MAX_CANCEL_KEY_LENGTH= 32 for 3.2) lives in the backend's ProcSignal slot;SendCancelRequest()compares it withtimingsafe_bcmp()and sendsSIGINT. - Tests:
src/test/modules/libpq_pipeline(pipeline mode, with trace output compared byte by byte),src/interfaces/libpq/t/,src/test/postmaster/t/004_negotiate.pl(SSL/GSS request negotiation). - Spec:
protocol.sgmlis the authoritative message-by-message reference.
Parameters that shape this
listen_addresses port unix_socket_directories plan_cache_mode password_encryption ssl authentication_timeout client_encoding
Related topics
Process model Client authentication SSL/TLS encryption Life of a query Client backend