{"post":{"slug":"advisory-locks-explained-2026","title":"Advisory Locks: Mutual Exclusion Without a Lock Table","excerpt":"You need exactly one worker running a job. A lock row in a table almost works and leaks on crashes. Advisory locks solve it properly using the database you already have.","category":"code","date":"August 14, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">F</span>our instances of your service each run a scheduled job every hour. You need exactly one of them to actually do the work.\n\nThe instinctive solution is a lock table: insert a row, do the job, delete the row. It works until an instance crashes mid-job, leaving a row nobody will delete, and every subsequent run skips the job until someone notices. Then you add timestamps and staleness checks, and now you are maintaining a distributed lock implementation.\n\nYour database already has one.\n\n## What changed in 2026\n\n- **Horizontal scaling made this ordinary.** Multiple instances of everything is the default deployment shape, so single-execution coordination became a routine requirement.\n- **Serverless made it harder.** Short-lived connections and aggressive pooling interact badly with session-scoped locks, which caught teams out.\n- **Pooler awareness improved.** Documentation and tooling got clearer about which pooling modes break advisory locks and why.\n- **Dedicated lock services stayed common.** Advisory locks remained the pragmatic choice for teams who would rather not run another piece of infrastructure.\n\n## How they work\n\nAn advisory lock is a lock the database holds on a number you supply. The database attaches no meaning to it — there is no table, no row, no data involved. It simply guarantees that only one holder can have a given number at a time.\n\nYou choose the number, typically by hashing a string like `nightly-report-job` into an integer. Every instance requests the same lock; one gets it, the others do not.\n\nTwo scopes, and the difference matters:\n\n**Session-scoped.** Held until explicitly released or the connection ends. This is the powerful property — if the process crashes, the connection drops and the lock releases automatically. No stale locks, no cleanup, no staleness heuristics.\n\n**Transaction-scoped.** Released when the transaction commits or rolls back. Simpler and safer where the work fits in one transaction, because there is no way to forget to release it.\n\n| | Lock table | Advisory lock |\n| --- | --- | --- |\n| Cleanup on crash | Manual, with staleness logic | Automatic |\n| Storage | A table and rows | None |\n| Contention | Row locks, possible bloat | In-memory |\n| Granularity | Whatever you model | Any integer |\n| Survives restart | Yes, which is the problem | No, which is the point |\n| Cross-database | Possible | No |\n\n## The two variants\n\n**Blocking.** Wait until the lock is available. Appropriate when the work must happen and order does not matter much — but a worker blocked indefinitely on a lock held by a stuck process is its own outage.\n\n**Try.** Attempt to acquire; return immediately with success or failure. For scheduled jobs this is almost always what you want: one instance gets the lock and works, the others get `false` and go back to sleep. No queue, no waiting, no pile-up.\n\nThe distinction matters more than it sounds. Using the blocking variant for an hourly job means that if one run hangs, every subsequent instance is stacked up waiting, and you discover this when connections run out.\n\n## Connection pooling is the trap\n\nSession-scoped advisory locks belong to a *connection*, not to your application logic. Connection poolers hand connections around between requests.\n\nThe failure modes are unpleasant and quiet. Acquire a lock, return the connection to the pool, and another request may get that connection while still holding your lock — or your release may run on a different connection and silently do nothing. In transaction-pooling mode, where a connection is only yours for the duration of a transaction, session-scoped locks are effectively unusable.\n\nThree ways out:\n\n**Use transaction-scoped locks.** They live and die with the transaction, which matches transaction pooling exactly. This is the simplest answer when the work fits in a transaction.\n\n**Hold a dedicated connection.** For long-running work, take a connection outside the pool for the lock's lifetime. Explicit, and it costs a connection.\n\n**Check your pooler's mode.** Session pooling preserves the semantics; transaction pooling does not. Know which you are running before relying on session locks — see [connection pooling explained](/blog/connection-pooling-explained-2026).\n\n## Common mistakes\n\n- **Session locks behind a transaction pooler.** Silently broken, intermittently.\n- **Blocking acquire for scheduled jobs.** One stuck run backs up everything behind it.\n- **Colliding lock numbers.** Two unrelated jobs hashing to the same integer serialise against each other for no reason. Namespace them.\n- **Assuming they work across databases.** Scoped to one instance; replicas do not share them.\n- **Forgetting to release session locks.** They persist for the connection's life, which in a pool may be a very long time.\n- **Using them for fine-grained data locking.** Row locks and [isolation levels](/blog/database-isolation-levels-2026) are the right tools for protecting rows.\n\n## FAQ\n\n**Do I need a dedicated lock service instead?**\nNot usually. If you already run a database and need single-execution coordination, advisory locks avoid another dependency. A dedicated service earns its place when you need locks independent of the database, cross-datacentre coordination, or features like lease renewal.\n\n**What happens during a failover?**\nAdvisory locks are in-memory and instance-scoped, so a failover loses them. Anything relying on them must tolerate a brief window where two instances could both acquire. For a scheduled job that usually means an occasional duplicate run — plan for idempotency, per [idempotency explained](/blog/idempotency-explained-2026).\n\n**How do I pick lock numbers?**\nHash a descriptive string, and use the two-integer form where available to namespace by application and job. Colliding with an unrelated component's number produces confusing serialisation that is hard to diagnose.\n\n**Can I see who holds a lock?**\nYes — most databases expose current locks in a system view, including advisory ones and the session holding them. That is the first place to look when a job stops running.\n\n## Where to go next\n\nFor the pooling behaviour that breaks session locks, read [connection pooling explained](/blog/connection-pooling-explained-2026). For the row-level locking these do not replace, [database isolation levels](/blog/database-isolation-levels-2026), and for the notification mechanism that pairs well with job coordination, [listen and notify](/blog/listen-notify-explained-2026).","hue":308,"metaTitle":"Advisory Locks: Mutual Exclusion Without a Table","metaDescription":"How database advisory locks give you distributed mutual exclusion without a lock table, why connection pooling breaks them, and when to use the try variant.","keywords":["advisory locks","postgres advisory lock","distributed lock database","job deduplication","leader election database","connection pooling locks","mutual exclusion","background job coordination 2026"],"tldr":["**The database enforces the lock on an arbitrary number you choose** — no table, no rows, no cleanup logic.","**Session-scoped locks release automatically when the connection dies** — which is exactly what a lock table cannot do.","**Connection pooling is the main hazard** — a session lock belongs to a connection, and pools hand connections around.","**Use the try variant for jobs** — blocking forever on a lock another worker holds is rarely what you want.","**Skip:** advisory locks across multiple databases. They are scoped to one instance and coordinate nothing beyond it."],"readTime":"5 min read","wordCount":1003},"related":[{"slug":"distributed-locks-explained-2026","title":"Distributed Locks Explained in 2026: How They Work","excerpt":"Distributed locks coordinate exclusive access across machines, not just threads. This guide explains leases, fencing tokens, and how to pick between Redis, etcd, and a plain database lock.","category":"code","date":"July 31, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":314},{"slug":"how-to-set-up-a-database-2026","title":"How to set up a database in 2026: the practical guide","excerpt":"Choosing and setting up a database is the first real architecture decision in any new project. Here is the 2026-correct path without wasted effort.","category":"code","date":"April 3, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":321},{"slug":"how-to-connect-to-a-database-2026","title":"How to connect to a database in 2026","excerpt":"Connecting to a database in Python, Node, or Go in 2026 takes about ten lines, but getting connection pooling, credentials, and TLS right takes a bit more thought.","category":"code","date":"February 23, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":314}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"t-plus-one-settlement-explained-2026","title":"T+1 Settlement Explained: What Faster Trades Changed","excerpt":"Trades now settle the next business day in major markets, with more following. Here is what actually changed for investors, and where the friction moved rather than vanished.","category":"finance","date":"August 15, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":274},"older":{"slug":"ai-inference-providers-compared-2026","title":"AI Inference Providers Compared: Who Serves Open Models","excerpt":"Hosted inference providers serve the same open models at very different speeds and prices. Here is what actually differs between them and how to evaluate one.","category":"ai","date":"August 14, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":200}}