Project Locks JavaScript SDK
Coordinate backend workers with renewable project lock leases using the Volcano JavaScript SDK.
Project locks coordinate backend functions in the same Volcano project. Use a service-role key; never expose it in browser code.
import { VolcanoAuth } from '@volcano.dev/sdk';
const volcano = new VolcanoAuth({
apiUrl: process.env.VOLCANO_API_URL,
anonKey: process.env.ANON_KEY,
accessToken: process.env.SERVICE_ROLE_KEY
});Run work while leader
const result = await volcano.locks.withLock(
'daily-rollup',
{ ttl: 30 },
async ({ signal }) => {
await runRollup({ signal });
}
);
if (result.error) {
throw result.error;
}
if (!result.acquired) {
return { skipped: true };
}withLock generates an ownership token, renews near one-third of the TTL with
jitter, and releases in finally. If renewal fails, it aborts signal and
returns an ownership error. The callback must stop when the signal is aborted.
Function handler example
export const handler = async () => {
const result = await volcano.locks.withLock(
'scheduled-cleanup',
{ ttl: 60 },
async ({ signal }) => {
const deleted = await deleteExpiredRecords({ signal });
return { deleted };
}
);
if (result.error) {
throw result.error;
}
return {
statusCode: 200,
body: JSON.stringify(
result.acquired
? { leader: true, ...result.data }
: { leader: false, skipped: true }
)
};
};When several functions run this handler concurrently, only the lease owner runs
deleteExpiredRecords. The others return leader: false.
Manage a lease directly
const acquired = await volcano.locks.acquire('migration', { ttl: 10 });
if (acquired.error) throw acquired.error;
if (!acquired.acquired) return;
try {
const renewed = await volcano.locks.renew('migration', acquired.lease, { ttl: 10 });
if (renewed.error) throw renewed.error;
await runMigration();
} finally {
const released = await volcano.locks.release('migration', acquired.lease);
if (released.error) console.error('release failed', released.error);
}Contention returns { acquired: false, error: null }. That covers both 409
codes: another live holder (lock_held) and a lapsed lease of your own that is
not yet reclaimable (lock_ownership_lost). Authentication, validation,
rate-limit, and availability failures return an error. Keep the lease object
private; its token proves ownership.
Recover an uncertain acquisition
Create ownership and request IDs before acquisition when the caller needs to recover an uncertain response:
const token = crypto.randomUUID();
const requestId = crypto.randomUUID();
const acquired = await volcano.locks.acquire('migration', { ttl: 30, token, requestId });The SDK retries a transport failure or HTTP 503 once with the same key, TTL, IDs, and credential. Other HTTP errors are not retried. Reuse those IDs for the same uncertain acquisition; use a new ownership token for a new lease after release or expiry. Keep the ownership token private.
Each lock method accepts requestId. withLock forwards acquisition IDs only
to acquisition and gives renewal and release their own request IDs. Equivalent
native examples are in the Python and Ruby
lock guides.
Reject writes from a displaced holder
lease.fencingToken rises whenever the lock changes hands and stays the same
across renewals. Pass it to whatever the lock protects and refuse writes that
carry a lower token than the highest already seen:
const result = await volcano.locks.withLock('rollup', { ttl: 30 }, async ({ lease }) => {
const { rowCount } = await sql`
update rollup_state
set cursor = ${next}, fencing_token = ${lease.fencingToken}
where id = ${id} and fencing_token <= ${lease.fencingToken}
`;
if (rowCount === 0) {
throw new Error('another holder took over');
}
});Without this, a holder whose renewal is delayed past expires_at can still write
after another process has taken the lock. The lease alone cannot prevent that.
Inspect or recover a lock
const { state } = await volcano.locks.get('migration');
// { held: true, expiresAt: '2026-07-20T14:00:10Z', fencingToken: 4503599627370497 }get needs no lock token, so a monitor or an operator script can read the holder
without owning the lease. held: false means an acquire would succeed now.
forceRelease drops the lease whatever token holds it, for a holder that died
without releasing. It breaks mutual exclusion by itself — the old holder keeps
working until its next renewal fails — so only use it where the protected
resource checks the fencing token.
After stopping the holder and confirming recovery is safe:
await volcano.locks.forceRelease('migration');For lock_rate_limited, error.retryAfter contains the number of seconds until
the fixed-minute window resets. A project gets 600 lock requests per minute —
reads and force releases included — and each holder spends 180 / ttl of them on
renewals, so a 30-second TTL supports roughly 100 concurrent holders. Increase the
TTL to raise that ceiling.
TTL may be 5 seconds through 90 days. For work that may outlive its requested
TTL, use withLock or renew explicitly. Renewing sets the new expiry outright
rather than adding to the current one, so passing a smaller TTL shortens the
lease. Renewal never extends the absolute 90-day lifetime of an acquisition;
acquire a new lease after that deadline. An unreleased lease still expires.
See the REST reference for guarantees and error
codes.