Pattern
April 19, 2026
11 min read
Maxime Dalessandro

lock_timeout best practices: what it catches and misses

lock_timeout is necessary for safe Postgres migrations and not sufficient. What a pre-merge migration check adds, and what only the timeout catches.

#PostgreSQL#Schema migrations#lock_timeout#Migration safety#CI/CD

lock_timeout is a correct, necessary, and well-documented Postgres setting. GoCardless published the canonical write-up in 2016 after their production API went down for 15 seconds during a ALTER TABLE. Nikolay Samokhvalov's 2021 Postgres.ai post made it authoritative: "Zero-downtime Postgres schema migrations need this: lock_timeout and retries." Every serious team running Postgres should be using it. It is also not enough on its own. The class of migration that takes down production can be flagged before it merges, with a different check that reads production state while the change is still in review. This post is about why those two layers complement each other, and what each one actually catches.

TL;DR. lock_timeout catches a migration mid-flight by aborting it after N seconds of waiting. It produces a visible failed deploy. A pre-merge migration check catches the same class of migration before the merge button is pressed, so the migration never reaches the deploy pipeline. Both layers are needed: the pre-merge check is the first line for the predictable cases, lock_timeout is the safety net for the cases the check misses. Neither replaces the other.

What lock_timeout actually does

The Postgres setting is simple. SET lock_timeout = '3s'; before a DDL tells Postgres to abort the statement if it has been waiting to acquire a lock for more than 3 seconds. The abort is immediate and clean; the DDL reports a canceling statement due to lock timeout error, no partial state is left behind, and any queries queued behind the DDL are released.

The canonical use, from the GoCardless post and the Samokhvalov follow-up, is:

SET lock_timeout = '3s';
SET statement_timeout = '30s';

ALTER TABLE sessions ADD COLUMN archived_at timestamptz;

If the ALTER TABLE cannot acquire ACCESS EXCLUSIVE within 3 seconds (because a reader is holding ACCESS SHARE, or because there is an unbounded queue in front of it as the earlier post on lock-queue fairness describes), Postgres aborts the DDL. The migration framework catches the error, marks the deploy failed, and the team retries later. No cascade, no queued-reader outage, no pool drain.

Tools like ActiveRecord SaferMigrations (GoCardless), Doctolib's safe-pg-migrations, and Xata's pgroll ship this pattern by default, typically with automatic retry and exponential backoff. A Postgres team in 2026 that is not setting lock_timeout in migrations is ignoring a decade of public operational wisdom.

What lock_timeout does not do

Three classes of migration that lock_timeout alone does not catch.

The migration that is going to take hours but acquires the lock quickly. lock_timeout limits how long the DDL waits for the lock. It does not limit how long the DDL runs once it has the lock. A CREATE INDEX without CONCURRENTLY on a billion-row table acquires SHARE immediately and holds it for 30 minutes while the build proceeds. lock_timeout = 3s does not save you. This is exactly the shape of Railway's October 28, 2025 incident, covered in the first post on this blog. The lock was acquired fast. The index build was slow.

The migration that produces cascades under the timeout. A lock_timeout of 3 seconds bounds the wait, but a 3-second queue on a heavily trafficked table can still exhaust the connection pool as readers accumulate. The migration aborts, but the cascade already peaked. For a 30-engineer SaaS with a 50-connection pool and 200 requests per second, 3 seconds is enough queue to drain the pool.

The migration that produces a visible failed deploy. This is the subtler one. Even when lock_timeout works perfectly and the migration aborts before any damage, the failed deploy is itself an operational signal. The deploy pipeline shows red. The team has to investigate. The migration has to be retried, possibly under different conditions. The friction is real, and it compounds: teams learn that migrations sometimes fail and build cultural workarounds (deploying at 3am, splitting migrations pre-emptively) that leak complexity across the organization. The operational cost is smaller than an outage but not zero.

What a pre-merge check adds

A migration's life drawn as one timeline: written, PR open, merged, deployed, runs in production, users feel it. Two interception points hang below the line. The pre-merge check intercepts while the PR is open: it reads live production state in review, and the unsafe migration never merges. lock\_timeout intercepts while the migration runs in production: it aborts the DDL after N seconds of waiting, the deploy fails and the queue releases. The check moves the decision left, to where the fix is a review comment instead of a failed deploy.

The two layers intercept at different points in the workflow.

A pre-merge migration check is a different layer, and any CI can host one. It reads production state before the merge, parses the proposed DDL, estimates the lock window against current traffic, and flags the risk while the change is still in review. The mechanics, described in more detail in the earlier posts on the staging gap and the lock-queue fairness rule, involve four pieces of context: the proposed DDL, the current contents of pg_locks and pg_stat_activity, the target table's size and row count, and a cost model for the specific DDL operation. The check itself is a parser and a few queries. The hard part is the context it needs about locks and workload, kept current enough to be trusted.

The check catches three specific classes that lock_timeout does not.

The slow-build migration. A CREATE INDEX on a billion-row table. The check reads the row count, applies the I/O-rate cost model, estimates a 30-minute build, and flags the PR with an add CONCURRENTLY suggestion. lock_timeout cannot catch this. The check can.

The cascade-under-timeout migration. An ALTER TABLE that will queue behind a currently-running long reader. The check reads pg_stat_activity, sees the reader, estimates the queue time, and flags the migration before it merges. lock_timeout = 3s might catch the cascade at run time, but the cascade already cost the pool. The check prevents the cascade from starting.

The migration that would cause a failed deploy. Any migration the check flags gets fixed before it reaches the deploy pipeline. The team amends the migration in the PR, re-runs the check, and merges once the flag clears. No failed-deploy signal reaches users, and no red deploy appears on the dashboard. A team that wants a hard stop can wire the check in as a required CI status; the value is in the timing either way.

What a pre-merge check does not do

Honest limits. The check is probabilistic, and the cases it cannot cover are the cases where lock_timeout is still necessary.

Production state shifts between review time and deploy time. The check reads pg_locks at the moment it runs. The developer reads the flag an hour later, fixes the issue, and merges. By the time the deploy runs, the lock graph may have changed. A new long-running reader may have started. The estimate from an hour ago is stale. lock_timeout at deploy time is the safety net for exactly this shift.

Uncommon migrations the cost model does not cover well. The cost model works well for common DDL (index builds, column additions, constraint additions) because the lock mechanics are well-understood and the cost is largely a function of table size. For less common operations (partition splits, inheritance changes, custom extension DDL), the model is less accurate, and its confidence interval should reflect that. A check that overclaims accuracy on operations it does not model well is worse than one that defers to lock_timeout plus human review.

Workload spikes. A migration that would be safe on a Tuesday afternoon is unsafe at 11am on Black Friday. The check can be aware of the deploy window, but it does not know what is going to happen in the next minute of production traffic. lock_timeout covers the surprise.

The long-tail behavioral change. A migration that adds a column is fast. A migration that adds a column with a default value on Postgres 10 or earlier is a table rewrite. A well-built check knows this; a careless developer may not. The check catches the common shape of this trap. It will not catch every version-specific behavioral quirk Postgres has accumulated over a decade. lock_timeout is the guard for the unknown.

The two-layer picture

The two layers catch different failure modes and stack cleanly. The pre-merge check is deterministic-ish and loud: it speaks up in review, names a risk, and suggests a fix. The lock_timeout is automatic and silent until it fires. The check moves the decision left, to the point where fixing is cheap. The lock_timeout is the last line of defense when the decision cannot be moved left in time.

Pre-merge check

When
before merge
Reads
live production state
Catches
slow builds, cascades, known patterns
Misses
state shift between review and deploy
Output
risk flag in review
Cost of a miss
pushes the check to layer 2

Post-deploy lock_timeout

When
during migration run
Reads
live lock queue
Catches
any prolonged wait
Misses
slow DDL that has the lock
Output
aborted DDL, failed deploy
Cost of a miss
cascade to pool, outage

Configuring both

The recommended stack for a Postgres team that has both layers running.

Migration runner. Use a framework that sets lock_timeout and statement_timeout per-migration automatically, and that retries with backoff when the timeout fires. GoCardless's ActiveRecord SaferMigrations, Doctolib's safe-pg-migrations, and Xata's pgroll all ship this by default. Samokhvalov's post is the standard reference for tuning the retry behavior.

Pre-merge check. A check in CI that reads live production state via a read-only Postgres role, parses the proposed DDL, and flags the risky migrations before merge. Any CI can host the check itself. The non-obvious pieces are the context it consumes: a cost model calibrated against your own production instance rather than TPC benchmarks, the current lock graph, and the workload on the target table. The same context serves a policy gate when the migration comes from an AI agent instead of a person: the gate needs to know what the change touches and what it will lock before deciding whether the agent may run it.

Monitoring layer. pganalyze, Datadog DBM, or equivalent, watching for post-deploy regressions the first two layers missed. These are the diagnostic signal for when both earlier layers were insufficient and a problem still reached production. Covered in detail in the earlier post on pg_stat_statements and auto_explain.

Three layers, each catching what the previous one missed. The cost is low because each layer is cheap to run. The benefit is that the migration that would have caused the outage gets filtered at the first layer that is capable of seeing it, not the last.

Closing note

The case for a pre-merge check is a case for layering, never a case against lock_timeout. lock_timeout is correct, necessary, and unconditional. Samokhvalov's post is required reading for anyone shipping Postgres migrations. What the pre-merge layer adds is the earlier interception: catching the migration before it reaches the point where lock_timeout has to decide whether to abort it. A team that runs both is protected against two different failure modes that each get through the other layer.

Datapace is building the context layer between your databases and your AI: resolved meaning validated by the people who own the data, the workload evidence beside it (cost, performance, usage and freshness, lineage), and a policy gate over what an agent may do and access, served over MCP. The lock graph, the workload, and the table sizes a pre-merge check needs are that same evidence, read from the same layer. If safe schema changes are the wedge you care about, start with the safe schema migrations use case.

Frequently asked questions

If lock_timeout catches the cascade, does a pre-merge check matter?

It catches it at a different cost. lock_timeout aborts the migration after N seconds of waiting, enough time for pool saturation to begin. A pre-merge check prevents the wait from starting: the cascade lock_timeout aborts has consumed connections, the one the check prevents has consumed nothing.

What value of lock_timeout should we use?

The Samokhvalov and GoCardless posts both recommend the shortest pause your application can tolerate, typically 1 to 3 seconds. Longer timeouts catch more cascades but raise the false-positive rate of aborted legitimate migrations. 3 seconds is a common default.

Can a pre-merge check replace lock_timeout entirely?

No. Production state shifts between review and deploy, and the check's estimate has a real error bar. A migration judged safe can meet a long-running reader that started after the check ran. lock_timeout is the cheap, deterministic guard for that case. Nothing replaces it.

Does pgroll already handle this?

Partly. pgroll applies DDL through versioned views with short lock_timeout and retries on the locking phases: a migration runner with the timeout built in. It does not read production state before the merge or flag a predicted cost. pgroll plus a pre-merge check is stronger than either alone.

Is this argument specific to schema migrations?

The same shape applies to any database change whose cost is a function of production state: large backfills, data-migration jobs, index builds outside the migration system, extension upgrades. The specifics of what the check reads differ per class; the two-layer argument is the same.

Sources

  1. N. Samokhvalov, "Zero-downtime Postgres schema migrations need this: lock_timeout and retries", Postgres.ai, September 2021.
  2. GoCardless Engineering, "Zero-downtime Postgres migrations: a little help" and "Zero-downtime Postgres migrations: the hard parts", 2016.
  3. Xata, pgroll GitHub repository.
  4. Doctolib, safe-pg-migrations.
  5. Railway, Incident report, October 28, 2025.
  6. Datapace blog, "ACCESS SHARE does not jump the queue: Postgres lock fairness".
  7. Datapace blog, "Why staging did not catch your slow migration".
  8. Datapace blog, "pgroll, pg-osc, pg_karnak, gh-ost: an online DDL tool face-off".

Keep reading

Ready to let agents touch production, safely?

Bring a use case. We will show you what agents can do on your live data, inside your guardrails.