Build Your Own PostgreSQL
The Wire Protocol and Session Lifecycle
What You Will Learn
- Explain why a PostgreSQL connection is a stateful protocol, not a stream of SQL strings
- Trace startup, authentication, query execution, transaction failure, and termination
- Distinguish the simple query path from prepared statements and portals
A socket is not yet a database connection
A bookstore service opens a TCP connection and sends SELECT title FROM books. It is tempting to imagine the server reading one SQL string, returning rows, and waiting for another. That model works for a toy console, but it leaves basic questions unanswered. Which user issued the query? Which database and settings apply? Is a transaction already open? If the statement fails, may the next statement run, or must the transaction first be abandoned?
A useful database connection must carry answers across messages. PostgreSQL therefore treats the frontend and backend as participants in a protocol, not as a writer and reader of unrelated text. The central invariant is agreement: after every complete exchange, both participants must agree about which messages are legal next and which session state remains in force. Startup, authentication, queries, results, errors, and termination are state transitions in that shared conversation.
Startup establishes the conversation
The first messages are not ordinary SQL. The frontend supplies startup information such as the requested user, database, and protocol parameters. The backend chooses an authentication exchange and either rejects the connection or establishes a session. Authentication answers who the client is; later privilege checks answer what that identity may do. Keeping those decisions separate matters because a valid user can still lack permission to read the bookstore's payments table.
After authentication, the backend reports initial parameter values and eventually announces that it is ready for a command. Ready is more than an idle socket. It includes transaction status: outside a transaction, inside a healthy transaction, or inside a failed transaction. A driver or pooler needs that fact before deciding whether a connection can safely accept new work or be handed to another request.
Session state machine
Trace one bookstore session
Client A connects as the reporting user, authenticates, and reaches Ready. It begins a transaction, so both sides now understand that subsequent statements belong to one unit of work. A valid sales query runs and returns to InTransaction. The next query names a nonexistent column. PostgreSQL reports the error and marks the transaction failed. Sending another business query cannot repair that state; the client must issue ROLLBACK, after which the backend can report Ready outside a transaction again.
That failure rule is deliberately stricter than 'the bad statement did nothing.' A statement might have performed work before discovering an error, and pretending the transaction remains usable would make atomicity depend on obscure execution details. The failed state gives clients a stable rule: once an explicit transaction has encountered an error, discard that attempted history before continuing. An error outside an explicit transaction can instead finish that single implicit transaction and return to Ready.
One query message or a reusable conversation
The simple query path sends SQL text as one request and receives its results. It is convenient for interactive use and statements executed once. The extended path separates parsing, parameter binding, describing, execution, and synchronization. A prepared statement retains parsed and analyzed intent; a portal combines a statement with parameter values and execution state. Portals also let a client fetch a result incrementally instead of requiring every row at once.
Neither path is universally better. The simple path has fewer conversational steps. The extended path supports typed parameters, reuse, and controlled result delivery. What must not leak into the public protocol is every internal planner or storage decision. Clients need stable concepts such as statement, parameter, result shape, transaction status, and readiness. They do not need to know which join algorithm won or which shared buffer held a page.
Interruption has more than one meaning
Cancellation asks a backend to stop its current operation while preserving the session if cleanup succeeds. The backend must notice the request at a safe point, unwind executor state, release statement resources, and report an error. Termination ends the session; an open transaction is rolled back and session-local objects disappear. A broken network connection has a similar cleanup obligation even though the polite termination message never arrived.
This distinction matters to connection pools. A pool cannot equate 'the previous application request ended' with 'the database session is clean.' Settings, prepared statements, temporary objects, and transaction state can outlive an application request. The pool must either reset that state or enforce a usage discipline. Ready status helps, but it does not erase every session object.
Three protocol rules
First, establish identity and initial context before accepting normal commands. Second, represent retained state and failure state explicitly enough that both participants can resynchronize. Third, expose durable conversational concepts while keeping execution machinery behind the boundary. With those rules, the protocol can evolve internally without making clients guess what the server believes.
A readiness checklist
Before sending the next message, a frontend should be able to answer four questions: did startup finish, is an exchange still awaiting completion or synchronization, what transaction status did the latest ready message report, and which session objects remain? Before returning a pooled connection, it must also know which state the pool promises to reset. These questions keep network ordering, transaction recovery, and session reuse from becoming one ambiguous idea called 'connected.'
The protocol does not prevent every client bug, but it gives a correct client enough boundaries to recover deliberately. A driver that ignores unread results or failed-transaction status can still misuse the conversation. Explicit states make that misuse observable and keep the backend from silently inventing a new interpretation.
Into SQL meaning
When SQL leaves the protocol layer, it is no longer anonymous text. It belongs to an authenticated session with settings and a transaction context. The next problem is semantic: turning that text into a statement about relations without confusing the order in which it is written with the order in which it must run.