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.
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.
$ curl "https://distributed-lock.com/v1/1/my-lock/acquire?waitTimeoutMs=5000"
{"acquired":true,"value":1}
// 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.
A few reasons it might be the easiest lock you add all year.
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.
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.
No SDK, no connection pool, no daemon to run. It's plain HTTP and JSON — call it with fetch, curl, or httpx from anywhere.
Set maxValue to 1 for classic mutual exclusion, or higher to cap concurrency at N — handy for rate limiting or bounding a worker pool.
Callers that have to wait are queued in order and handed the lock the instant a slot frees up — no busy-polling required.
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.
Run your critical section knowing you're the only one — or one of at most maxValue callers — holding this lock.
GET /release when you're done, freeing your slot for the next caller in line.
4 endpoints. All GET. All JSON. No auth.
https://distributed-lock.com
/v1/{maxValue}/{lockId}/{action}
| Parameter | Type | Description |
|---|---|---|
maxValue | positive integer | Concurrent-holder cap. Use 1 for a plain mutex, or higher for a counting semaphore. Part of the lock's identity — see Limits below. |
lockId | string, ≤ 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. |
action | acquire · release · value · reset | The operation to perform. |
/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 param | Type | Default | Description |
|---|---|---|---|
waitTimeoutMs | integer, milliseconds, ≥ 0 | 0 | How long to wait for a slot before giving up. 0 means "try once, don't wait." Case-insensitive. |
{ "acquired": true, "value": 1 }
{ "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.
/v1/{maxValue}/{lockId}/release
Releases one held slot.
{ "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.
/v1/{maxValue}/{lockId}/value
Reads the current number of active holders.
{ "value": 1 }
{}
/v1/{maxValue}/{lockId}/reset
Force-clears the lock: every queued acquire immediately resolves with acquired: false, and the counter drops back to 0.
{ "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.
| Status | When |
|---|---|
400 | maxValue isn't a positive number, lockId is over 256 characters, or waitTimeoutMs isn't a non-negative number. |
404 | The path doesn't match /v1/{maxValue}/{lockId}/{action}, or action isn't one of the four above. |
405 | Any method other than GET — including acquire and release, there's no request body so GET is all you need. |
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.(maxValue, lockId) pair — mixing maxValues for "the same" lock silently gets you two different locks.Try to acquire, do your work if you got it, then release.
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`);
}
}
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")
A few patterns worth following, and a few worth avoiding.
waitTimeoutMs plus a few seconds of slack for network latency.finally block, so an exception in your critical section can't leak the lock forever.uuid4()) so strangers can't collide with — or hijack — your lock.maxValue every time you refer to a given lockId.maxValue > 1 when you need bounded concurrency instead of strict mutual exclusion./release or /reset is called./acquire after a timeout or network error — a duplicate request that lands twice can double-acquire a slot.lockId as private — there's no ownership token, so anyone who knows it can release or reset it too./reset as part of normal control flow — it force-fails every current waiter; save it for recovering a stuck lock.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.
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:
waitTimeoutMs so a slow-but-healthy response isn't mistaken for a dead connection./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).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.
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.
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.
The backend has zero line of code written by AI. Only this page is AI written.