It's free

Distributed lock over HTTP

Acquire, release, and inspect distributed locks — or counting semaphores — with a single HTTP request. No infrastructure to run, no client library to install, and low latency.

shell
$ curl "https://distributed-lock.com/v1/1/my-lock/acquire?waitTimeoutMs=5000"
{"acquired":true,"value":1}

Try it, live

Lock ID maxValue1
Not checked yet
// responses will show up here

This demo calls acquire with waitTimeoutMs=0 — it tries once and doesn't wait. Click Acquire twice in a row to see it fail until you Release.

Why use it?

A few reasons it might be the easiest lock you add all year.

Low latency

Each lock spins up on the first request that touches it — right where that request came from. Every acquire and release after that is a short hop, not a trip across the globe.

Highly available

There's no origin server for this to depend on. It's built entirely on Cloudflare's network — if this goes down, Cloudflare itself is down.

Zero setup

No SDK, no connection pool, no daemon to run. It's plain HTTP and JSON — call it with fetch, curl, or httpx from anywhere.

Mutex or semaphore

Set maxValue to 1 for classic mutual exclusion, or higher to cap concurrency at N — handy for rate limiting or bounding a worker pool.

Fair queueing

Callers that have to wait are queued in order and handed the lock the instant a slot frees up — no busy-polling required.

How it works

  1. 1

    Acquire

    GET /acquire with an optional waitTimeoutMs. If a slot is free you get it back immediately. If not, the request stays open and queues you fairly until a slot frees up or your timeout elapses.

  2. 2

    Do your work

    Run your critical section knowing you're the only one — or one of at most maxValue callers — holding this lock.

  3. 3

    Release

    GET /release when you're done, freeing your slot for the next caller in line.

API reference

4 endpoints. All GET. All JSON. No auth.

Base URL https://distributed-lock.com
Path shape /v1/{maxValue}/{lockId}/{action}
Shared path parameters
ParameterTypeDescription
maxValuepositive integerConcurrent-holder cap. Use 1 for a plain mutex, or higher for a counting semaphore. Part of the lock's identity — see Limits below.
lockIdstring, ≤ 256 chars, no /The lock's name. Public: anyone who knows it can operate on it. Use an unguessable value (e.g. uuid4()) if it shouldn't be shared.
actionacquire · release · value · resetThe operation to perform.
GET /v1/{maxValue}/{lockId}/acquire

Tries to acquire the lock. Succeeds immediately if a slot is free; otherwise queues you and holds the connection open until a slot frees up or waitTimeoutMs elapses.

Query paramTypeDefaultDescription
waitTimeoutMsinteger, milliseconds, ≥ 00How long to wait for a slot before giving up. 0 means "try once, don't wait." Case-insensitive.
Response — acquired
{ "acquired": true, "value": 1 }
Response — timed out
{ "acquired": false }

Tip: set your HTTP client's own timeout to waitTimeoutMs plus a few seconds of slack for latency. The server always resolves the request by itself once waitTimeoutMs elapses — you're just giving the response time to travel back to you.

GET /v1/{maxValue}/{lockId}/release

Releases one held slot.

Response
{ "released": true }

Note: there's no ownership token — anything that knows lockId can release it. released is false only when the count is already at 0.

GET /v1/{maxValue}/{lockId}/value

Reads the current number of active holders.

Response
{ "value": 1 }
Response — never acquired / just reset
{}
GET /v1/{maxValue}/{lockId}/reset

Force-clears the lock: every queued acquire immediately resolves with acquired: false, and the counter drops back to 0.

Response
{ "reset": true }

Warning: this is a recovery tool, not a normal-flow operation — it fails every current waiter. Use it to unstick a lock, not as part of your regular acquire/release cycle.

Errors

StatusWhen
400maxValue isn't a positive number, lockId is over 256 characters, or waitTimeoutMs isn't a non-negative number.
404The path doesn't match /v1/{maxValue}/{lockId}/{action}, or action isn't one of the four above.
405Any method other than GET — including acquire and release, there's no request body so GET is all you need.

Limits

  • lockId: max 256 characters, a single path segment (no /).
  • maxValue: positive integer, no fixed upper bound — pick what your use case needs.
  • waitTimeoutMs: milliseconds, default 0, no fixed upper bound — see the timeout tip above.
  • One lock lives per unique (maxValue, lockId) pair — mixing maxValues for "the same" lock silently gets you two different locks.

Code examples

Try to acquire, do your work if you got it, then release.

lock.ts
const lockId = crypto.randomUUID(); // don't share this with other users
const baseurl = `https://distributed-lock.com/v1/1/${lockId}`;

const { acquired } = await fetch(`${baseurl}/acquire?waitTimeoutMs=5000`).then((r) => r.json());

if (acquired) {
  try {
    // do your work
  } finally {
    // Always release if you acquired, even if your work threw an exception.
    await fetch(`${baseurl}/release`);
  }
}
lock.py
import uuid
import httpx

lock_id = str(uuid.uuid4())  # don't share this with other users
baseurl = f"https://distributed-lock.com/v1/1/{lock_id}"
wait_timeout_ms = 5000
client_timeout_s = wait_timeout_ms / 1000 + 5  # add a few seconds to account for network latency

async with httpx.AsyncClient(timeout=client_timeout_s) as client:
    response = await client.get(f"{baseurl}/acquire", params={"waitTimeoutMs": wait_timeout_ms})
    if response.json()["acquired"]:
        try:
            # do your work
        finally:
            # Always release if you acquired, even if your work threw an exception.
            await client.get(f"{baseurl}/release")

Good to know

A few patterns worth following, and a few worth avoiding.

Do

  • Pad your client-side timeout to waitTimeoutMs plus a few seconds of slack for network latency.
  • Always release inside a finally block, so an exception in your critical section can't leak the lock forever.
  • Generate unguessable lock IDs (e.g. uuid4()) so strangers can't collide with — or hijack — your lock.
  • Reuse the exact same maxValue every time you refer to a given lockId.
  • Use maxValue > 1 when you need bounded concurrency instead of strict mutual exclusion.

Avoid

  • Relying on auto-expiry — there isn't any. A crashed holder keeps its slot until /release or /reset is called.
  • Auto-retrying /acquire after a timeout or network error — a duplicate request that lands twice can double-acquire a slot.
  • Treating lockId as private — there's no ownership token, so anyone who knows it can release or reset it too.
  • Calling /reset as part of normal control flow — it force-fails every current waiter; save it for recovering a stuck lock.

FAQ

Is it free?

Yes, for now — this project is not for profit. If usage grows enough that hosting costs become a real burden, monetisation might be introduced to help cover them, so subscribe to the email list to stay informed.

How do I handle request errors (not server errors)?

A "request error" means the HTTP call itself failed before you got a response back — a timeout, a dropped connection, a DNS hiccup. The tricky part: the server may have already processed the request even though you never saw the response. For /acquire, that means the lock might actually be held even though your client thinks it errored.

A few rules of thumb:

  • Set a client-side timeout longer than waitTimeoutMs so a slow-but-healthy response isn't mistaken for a dead connection.
  • Treat a request error on /acquire as unknown, not failed — don't assume it's safe to immediately retry, since the original call might still succeed server-side a moment later.
  • /value and /release are safe to retry freely — reading has no side effects, and releasing an already-released lock is just a no-op (released: false).
Is this a mutex or a semaphore?

Both — maxValue controls it. A semaphore is a generalization of a mutex: instead of allowing only one holder at a time, it allows an arbitrary number of concurrent holders up to a limit. Set maxValue to 1 for a classic mutual-exclusion lock, or higher to allow that many concurrent holders as a counting semaphore. The rest of the API (acquire / release / value / reset) works identically either way.

What happens if a holder crashes without releasing?

Nothing releases it for you — there's no TTL or auto-expiry. The slot stays held until someone calls /release, or you wipe it with /reset. Always release in a try…finally, and treat /reset as your manual recovery button for a stuck lock.

Do I need an API key or account?

No — there's no auth. That also means lock IDs work like capabilities: anyone who knows a lockId can acquire, release, or reset it, so generate an unguessable one (e.g. uuid4()) instead of a predictable name.

Is it AI slop?

The backend has zero line of code written by AI. Only this page is AI written.