Lock

createLock() is an in-process per-key mutex. Same key serializes; different keys run in parallel. The waiter queue is FIFO; handler errors propagate to the caller and unblock the next waiter.

For cluster-wide locks, use createDistributedLock from the extensions package - the API shape is the same so callers can swap implementations without changing call sites.

Setup

// src/lib/server/lock.js
import { createLock } from 'svelte-adapter-uws/plugins/lock';

export const lock = createLock();

Usage

import { lock } from '$lib/server/lock';

const order = await lock.withLock(`order:${orderId}`, async () => {
  // Only one caller at a time per orderId.
  const current = await db.getOrder(orderId);
  return processOrder(current);
});

Calls with the same key serialize; calls with different keys run in parallel. The lock is released automatically when the handler resolves or throws.

Bounded wait - maxWaitMs

try {
  const result = await lock.withLock(`order:${orderId}`, handler, { maxWaitMs: 5000 });
} catch (err) {
  if (err.code === 'LOCK_TIMEOUT') {
    // Did not acquire within 5000 ms. err.key, err.maxWaitMs available.
    return retry();
  }
  throw err;
}

Past maxWaitMs, the waiter rejects with LiveError('LOCK_TIMEOUT', ...) carrying .code, .key, and .maxWaitMs. The current holder is NOT interrupted; subsequent waiters are unaffected.

lock.clear()

Clears the entire lock state and rejects every pending waiter with LOCK_CLEARED:

process.on('SIGTERM', () => lock.clear());
await someLockedCall().catch(err => {
  if (err.code === 'LOCK_CLEARED') return; // shutdown is in progress
  throw err;
});

API

MethodDescription
withLock(key, fn, { maxWaitMs? })Run fn exclusively per key. Returns the handler’s return value. Rejects with LOCK_TIMEOUT if maxWaitMs elapses.
clear()Drop all state. Pending waiters reject with LOCK_CLEARED.

Options

createLock(options?) accepts:

OptionDefaultDescription
maxKeyLength256Reject withLock(key, ...) calls where key.length > maxKeyLength synchronously with LiveError('INVALID_KEY', ...). Pass Infinity to disable.
maxWaitersPerKey1000Flood control. Past this many pending waiters for the same key, new acquires reject synchronously with LiveError('LOCK_QUEUE_FULL', ...) instead of growing the queue.

The 256-char maxKeyLength is a defense-in-depth bound: a 1 MB wire-supplied key would otherwise anchor a 1 MB heap entry per active lock plus replicate that key across every waiter promise’s closure. The cap rejects with the actual length so callers can log the offender.

maxWaitersPerKey bounds memory cost per hot key. In-flight callers (current holder plus existing waiters) are unaffected; only new admission rejects. Keys consistently at the cap usually indicate unbounded fan-in to a single sequenced resource - the application-level fix is sharding, queueing, or removing the lock.

import { LiveError } from 'svelte-realtime/server';

try {
  await lock.withLock(`hot:${id}`, fn);
} catch (err) {
  if (err.code === 'LOCK_QUEUE_FULL') {
    // err.key, err.maxWaitersPerKey
    return reply.tooManyRequests();
  }
  throw err;
}

See also

  • live.lock - the realtime wrapper that uses this lock as its in-process backing.
  • createDistributedLock - Redis-backed cluster-wide variant with the same API shape.

Was this page helpful?