Distributed Locks

One active holder per task, at any scale

Renewable leases that let one agent, worker or durable function hold a task at a time, with fencing tokens so a holder that lost its lease cannot overwrite the one that took over.

locks
# three agents reach for the same ticketagent-a  withLock('ticket:4821')  acquired · fencing 17agent-b  withLock('ticket:4821')  acquired: false · takes ticket:4822agent-c  withLock('ticket:4821')  acquired: false · takes ticket:4823agent-a  lease renewed every 10s while it works✓ released · ticket 4821 handling complete
Overview

Scale out, and the same task runs twice

Add a second worker and the races start. Two agents answer the same ticket, two durable executions sync the same account, two overlapping runs apply the same migration. The usual fix is a separate lock service and a hand-rolled lease that forgets to renew, or a database row locked for the whole length of the job.

Volcano gives every project named locks. A holder takes a lease with a TTL, the SDK renews it while the work runs and releases it when the work ends, and every other caller gets acquired: false straight away instead of waiting. If the holder dies, its lease expires and the next caller takes over. A fencing token that rises with each new holder lets the data you protect refuse a write from a holder that already lost the lock.

Leases

Held while the work runs, released when it ends

withLock takes the lease, renews it at about a third of the TTL, and releases it when your callback returns. If a renewal fails, it aborts the signal it handed your callback, so the work stops instead of carrying on without the lock.

  • TTLs from 5 seconds to 90 days
  • A held lock answers acquired: false, it never blocks
  • A crashed holder's lease expires on its own
one holder, start to finish
await volcano.locks.withLock('nightly-rollup', { ttl: 30 }, run)acquired   expires in 30s · fencing 4503599627370497renewed    expires in 30srenewed    expires in 30sreleased · the next run can take it
Fencing

A holder that lost the lock cannot write

A lease cannot stop a process that stalled past its expiry. The fencing token can. It rises every time the lock changes hands, so a guarded write that carries an older token changes nothing and the stale holder knows to stop.

  • Rises with every new holder, not on renewals
  • One conditional UPDATE to enforce it
  • Keeps a forced release safe
two holders, one row
update job_state set cursor = $1, fencing_token = $2  where id = $3 and fencing_token <= $2;holder with token 18 (current)   UPDATE 1holder with token 17 (stalled)   UPDATE 0 · stopsthe stale write was refused by the data itself
Scale

A fleet that shares work without a queue

Give each task its own key and any number of agents, workers or durable executions can reach for work at once. The first acquire wins, the rest move on to the next task, and a task whose worker died becomes available again when its lease runs out.

  • A key per task, like ticket:4821
  • Scoped to the project, so keys never collide across projects
  • Longer TTLs keep more holders alive at once
three agents, one backlog
agent-a  ticket:4821  acquiredagent-b  ticket:4821  acquired: false → ticket:4822 acquiredagent-c  ticket:4822  acquired: false → ticket:4823 acquiredagent-a  crashed · lease on ticket:4821 runs out in 60sticket:4821 taken over by agent-b · fencing 18
Capabilities

What every lock gives you

Renewable leases

Acquire with a TTL from 5 seconds to 90 days, renew while you work, release when you are done.

withLock

Acquire, renew and release in one call, with an abort signal the moment ownership is lost.

Fencing tokens

A rising token per lock key, so the resource you protect can refuse a holder that lost its lease.

Non-blocking contention

A held lock answers acquired: false at once, so a worker moves on to other work instead of waiting.

Inspect and recover

Read whether a lock is held without owning it, and force-release the lease of a holder that died.

JavaScript, Python and Ruby

The same leases from every SDK, and a REST API for anything else that speaks HTTP.

Code

Take it, fence it, share it

JavaScript
const { VolcanoAuth } = require('@volcano.dev/sdk'); // Locks take a service key, so run this in a function or on your own server. const volcano = new VolcanoAuth({ apiUrl: process.env.VOLCANO_API_URL, anonKey: process.env.VOLCANO_ANON_KEY, accessToken: process.env.VOLCANO_SERVICE_KEY, }); // Every agent runs this over the same backlog. At most one agent holds a // ticket's lease at a time; the others get acquired: false and move on. async function takeNextTicket(tickets) { for (const ticket of tickets) { const result = await volcano.locks.withLock( `ticket:${ticket.id}`, { ttl: 60 }, ({ signal, lease }) => answerTicket(ticket, { signal, fencingToken: lease.fencingToken }), ); if (result.error) throw result.error; if (result.acquired) return result.data; } return null; }
Use cases

What people coordinate with locks

support-agents · backlog of 40 tickets
12 agents running40 tickets claimed · 0 concurrent claims1 agent crashed · its ticket reclaimed after 60s✓ backlog cleared, every ticket answered
Platform

Works with the rest of Volcano

Durable functions
  • Coordinate executions

    Take a lock inside a durable step so only one execution works on a shared resource at a time.

Functions and agents
  • Elect a leader

    Wrap a scheduled or fanned-out function in withLock, and the runs that lose skip instead of doubling up.

Databases and vector
  • Fence your writes

    Keep the fencing token beside the rows the lock protects, and one conditional UPDATE refuses stale holders.

Realtime
  • Tell the others

    Broadcast when a holder finishes, so waiting agents pick up the next task without polling.

Frequently asked questions

Read the docs
Who can take a lock?

Trusted code holding the project's service key: a function, a durable function or your own backend. Locks are scoped to the project, so two projects can use the same key without meeting, and browser credentials cannot take them.

What happens if the holder crashes?

Its lease stops being renewed and expires at its TTL. One second later, a grace period for clock skew, the next caller can take it. Choose the TTL for how long you are willing to wait before a dead holder's task is picked up again.

Does acquiring wait for the lock to be free?

No. A held lock answers acquired: false at once in JavaScript and raises ConflictError in Python, so a worker can move on to another task or try again on its own schedule.

Why do I need fencing tokens?

Because a lease cannot stop code that is already running. A holder that pauses past its expiry, in a long garbage collection or a slow network call, can wake up after someone else took the lock. The fencing token rises with each new holder, so a write guarded by it is refused when it comes from the older one.

How many locks can a project hold at once?

A project gets 600 lock operations a minute, renewals included. withLock renews at a third of the TTL, so a 30-second TTL keeps about 100 holders alive at once and a 60-second TTL about 200. Longer TTLs raise that further, at the cost of a slower takeover after a crash.

How is this different from a durable function's execution name?

An execution name makes one start idempotent: the same name never starts twice. A lock coordinates work that runs in many places at once, like several agents or executions reaching for the same resource. Use both: name executions after the task, and lock the shared resource they touch.

Ready to coordinate your workers?

Build, deploy, and scale on Volcano's global platform — free to start, with no infrastructure to manage.

Checkout more features