When a network or server failure occurs mid-transaction, PostgreSQL may raise ERROR: could not send query due to connection failure (or a similar driver-level message) alongside SQLSTATE 40003, condition name statement_completion_unknown. This error signals that a statement was in-flight when the connection broke, and it is impossible to determine from the client side whether the server committed or rolled back the work.
What This Error Means
SQLSTATE 40003 belongs to PostgreSQL's Class 40 — Transaction Rollback. The class groups errors that cause (or are caused by) the current transaction being aborted. The specific condition statement_completion_unknown sits alongside 40001 (serialization_failure) and 40002 (transaction_integrity_constraint_violation) in that class.
The critical property of 40003 is its ambiguity: the client cannot know what happened on the server. Compare this to 40001, where you know the transaction was rolled back and can safely retry. With 40003, retrying without first checking server state risks double-committing a write — for example, charging a customer twice or inserting a duplicate record.
This situation typically arises during the TCP tear-down or timeout that occurs after a statement has been sent to the server but before the server's response (CommandComplete message) arrives at the client. The PostgreSQL server may have fully committed the statement, partially committed it, or rolled back — the client simply has no acknowledgment either way.
Common Causes
- Network partition or dropped TCP connection — A firewall, load balancer, or NAT gateway drops the connection (idle timeout, packet loss) between the time the client sends
COMMITand the time the server's acknowledgment arrives. - Server crash or OOM kill mid-transaction — The PostgreSQL backend process is killed (e.g., by the OOM killer or a
pg_terminate_backend()call) after it has begun flushing the transaction to WAL but before it has replied to the client. - Statement timeout or
idle_in_transaction_session_timeoutfiring — PostgreSQL terminates the session due to a timeout at a moment that the client interprets as indeterminate completion. - Connection pooler or proxy interruption — PgBouncer, pgpool-II, or an application-layer proxy severs the connection and returns an error before the upstream server response is forwarded.
- Client-side network timeout — The application's TCP socket times out waiting for the server's reply, even though the server eventually committed successfully.
How to Fix statement_completion_unknown
Never blindly retry without checking — Before retrying the operation, reconnect and query the database to determine whether the previous transaction actually committed:
-- If your transaction wrote to a specific table, check for the record: SELECT COUNT(*) FROM orders WHERE idempotency_key = 'abc-123';Design for idempotency — Add an idempotency key or a unique constraint to writes that must not be duplicated. On reconnect, attempt an
INSERT ... ON CONFLICT DO NOTHINGor an upsert:INSERT INTO payments (idempotency_key, amount, status) VALUES ('txn-xyz-789', 5000, 'completed') ON CONFLICT (idempotency_key) DO NOTHING;Use two-phase commit (2PC) where appropriate — For distributed transactions where ambiguity is unacceptable, prepare the transaction before committing so you can query
pg_prepared_xactsafter reconnection:-- Before the connection might drop: PREPARE TRANSACTION 'my-txn-id'; -- After reconnect, check if the prepared transaction exists: SELECT gid FROM pg_prepared_xacts WHERE gid = 'my-txn-id'; -- Then commit or roll back deterministically: COMMIT PREPARED 'my-txn-id'; -- or ROLLBACK PREPARED 'my-txn-id';Audit driver and connection pool timeout settings — Ensure application-level socket timeouts (
connect_timeout,statement_timeout) are longer than expected query execution time, or handle40003explicitly in the error handler to trigger an idempotency-safe retry path:import psycopg2 from psycopg2 import errorcodes try: conn.commit() except psycopg2.OperationalError as e: if e.pgcode == errorcodes.T_R_STATEMENT_COMPLETION_UNKNOWN: # '40003' # Do NOT retry blindly — check server state first verify_commit_status(idempotency_key) else: raiseCheck PostgreSQL server logs — The server-side log (at
log_min_messages = erroror lower) will record whether the transaction committed, including the LSN. This is the authoritative source of truth when the client is uncertain.
Additional Information
- SQLSTATE class
40errors are defined in the SQL standard. PostgreSQL documents them in Appendix A of the PostgreSQL manual under "Transaction Rollback." - Related codes in the same class:
40001(serialization_failure, retryable in serializable/repeatable read),40002(transaction_integrity_constraint_violation),40P01(deadlock_detected). Only40003is inherently ambiguous about commit status. - Most PostgreSQL client drivers (libpq, psycopg2, asyncpg, JDBC) surface
40003via their generic connection error path. Some drivers may map it to a generalOperationalErrorwithout preserving the SQLSTATE unless the application inspectspgcode/getSQLState(). - In high-availability setups with automatic failover (Patroni, repmgr), a primary crash can produce
40003if a failover occurred between theCOMMITbeing written to WAL on the primary and the client receiving confirmation. Replays of the committed transaction on the new primary may still be consistent, but the client will not know this without checking. - Two-phase commit (
PREPARE TRANSACTION) is the only built-in PostgreSQL mechanism that definitively resolves40003ambiguity. It requiresmax_prepared_transactions > 0inpostgresql.conf.
Frequently Asked Questions
Is it safe to retry the statement when I get SQLSTATE 40003?
No — not without first verifying the outcome. Unlike 40001 (serialization failure), which guarantees the transaction was rolled back, 40003 means the commit outcome is unknown. Retrying a write blindly can result in duplicate data. Always reconnect and check your idempotency key or a unique constraint before retrying.
How is 40003 different from a normal connection error?
A plain connection error (e.g., 08006 connection_failure) means no statement was in flight, so you know nothing was committed. 40003 specifically indicates a statement was executing when the connection broke — the ambiguity is the distinguishing characteristic. Your error handler should treat them differently.
Why does PostgreSQL not just roll back on disconnect?
PostgreSQL does roll back uncommitted transactions when a client disconnects unexpectedly. However, if the server completed the commit (wrote to WAL and sent CommandComplete) but the network dropped before the client received the reply, the transaction is durably committed on the server. The client has no way to know this happened, which is precisely what 40003 reports.
Does connection pooling make 40003 more likely?
Connection poolers that aggressively time out or multiplex connections can increase the frequency of 40003. PgBouncer in transaction-pooling mode is a common source because the pooler may reassign or drop the server connection between the COMMIT and its acknowledgment. Configuring appropriate server_idle_timeout and ensuring statement-level error handling in the application helps mitigate this.