Fixing PostgreSQL 'cannot execute INSERT in a read-only transaction' Errors

intermediate🐘 PostgreSQL2026-07-19| PostgreSQL 10+, Linux (Ubuntu/Debian/CentOS), Docker, AWS RDS, Google Cloud SQL

Error Message

ERROR: cannot execute INSERT in a read-only transaction
#postgresql#database-administration#aws-rds#pgbouncer#sql-errors

Why This Error Happens

Seeing the ERROR: cannot execute INSERT in a read-only transaction message usually means your application is trying to write data to a database node that isn't allowed to accept it. In modern cloud setups, this almost always happens because your app accidentally connected to a Standby (Replica) instead of the Primary (Writer) node.

PostgreSQL replicas use 'hot standby' mode. This allows them to handle heavy SELECT traffic to take the load off your main server. However, they strictly block INSERT, UPDATE, or DELETE commands to keep data consistent with the primary source.

Step 1: Run a Quick Role Check

Don't guess which server you are on. You can ask PostgreSQL directly if it is currently in recovery mode. This is the fastest way to confirm if you're hitting a replica.

Execute this query in your SQL tool (like psql or DBeaver):

SELECT pg_is_in_recovery();
  • f (false): You are on the Primary node. If writes still fail, your session or database is explicitly set to read-only mode.
  • t (true): You are on a Replica. You need to point your connection elsewhere.

Step 2: Audit Your Connection Endpoints

If the check above returned t, your application is knocking on the wrong door. Check your .env, application.yml, or Kubernetes ConfigMaps for the database host URL.

Cloud Platforms (AWS RDS / Aurora)

Managed services typically provide two distinct URLs. For example, in AWS Aurora, you might see:

  • Writer Endpoint: mydb.cluster-xyz.us-east-1.rds.amazonaws.com (Use this for your main app).
  • Reader Endpoint: mydb.cluster-ro-xyz.us-east-1.rds.amazonaws.com (Use this only for reporting or analytics).

Make sure your write-heavy microservices aren't accidentally using the Reader address.

Step 3: Use Smart Routing in Connection Strings

If you use libpq-based drivers—common in Python, Go, and Node.js—you can let the client find the writer for you. List multiple hosts and add the target_session_attrs parameter.

Update your connection string to look like this:

postgresql://user:pass@host-a:5432,host-b:5432/dbname?target_session_attrs=read-write

The client will try host-a first. If it finds a replica, it will automatically jump to host-b to check for a writable node. This prevents downtime during a database failover.

Step 4: Fix PgBouncer Configuration

If you use PgBouncer for connection pooling, the mistake might be in your pgbouncer.ini. It is common to define separate pools for different roles. A misconfiguration might look like this:

[databases]
# The app should be hitting 'primary_db', not 'analytics_db'
primary_db = host=10.0.0.1 port=5432 dbname=prod
analytics_db = host=10.0.0.2 port=5432 dbname=prod

Double-check that your application's database port (usually 6432 for PgBouncer) is routing to the correct alias.

Step 5: Check for Code-Level Read-Only Flags

Sometimes the hardware is fine, but the software is restricting itself. In Java Spring Boot, for instance, a developer might have added @Transactional(readOnly = true) to a service method that now needs to perform a write.

You can check the current session's status with this command:

SHOW default_transaction_read_only;

If it returns on, but pg_is_in_recovery() is false, someone may have restricted the user permissions or the global config. You can force it off for your current session using:

SET default_transaction_read_only = off;

How to Verify the Fix

Once you believe the routing is fixed, run a manual test on the same connection string your app uses. Create a tiny dummy table to be sure:

CREATE TABLE connection_test (id serial PRIMARY KEY, test_val text);
INSERT INTO connection_test (test_val) VALUES ('success');
DROP TABLE connection_test;

If that INSERT works, your application is finally talking to the right node.

Proactive Prevention

  • DNS Clarity: Use explicit names like db-writer.internal and db-reader.internal instead of raw IP addresses.
  • Monitoring: Set up an alert in Prometheus or CloudWatch to trigger if your primary node's pg_is_in_recovery status ever flips to true.
  • Framework Awareness: Ensure your ORM isn't defaulting to read-only transactions to "optimize" performance.

Related Error Notes