A distributed FIFO lock server with leases and semaphores. Single Go binary, zero runtime dependencies. Speaks a line-based TCP protocol and an optional HTTP REST API — both backed by the same in-memory LockManager, so a TCP client and an HTTP session contending on the same key are ordered together in a single FIFO queue.
Documentation · HTTP API · OpenAPI · Changelog
go install github.com/mtingers/dflockd/cmd/dflockd@latest
dflockd # TCP only, on 127.0.0.1:6388
dflockd --http-port 6389 # TCP + HTTP RESTOr from a checkout:
make build && ./dflockdimport (
"context"
"time"
"github.com/mtingers/dflockd/client"
)
l := &client.Lock{
Key: "deploy-job",
AcquireTimeout: 10 * time.Second,
LeaseTTL: 60, // seconds; 0 = server default
Servers: []string{"127.0.0.1:6388"},
}
got, err := l.Acquire(context.Background())
if err != nil { /* network error */ }
if !got { /* timed out */ }
defer l.Release(context.Background())Lock.Acquire runs background lease renewal at half the TTL, so a holder that crashes loses the lock within LeaseTTL seconds without explicit pings. Same shape for Semaphore plus a Limit field. See the Go client docs for the full API (two-phase enqueue/wait, sharding, TLS, auth).
sid=$(curl -sX POST http://localhost:6389/v1/sessions | jq -r .session_id)
curl -sX POST http://localhost:6389/v1/locks/deploy-job \
-H "X-Dflockd-Session: $sid" \
-d '{"acquire_timeout_s": 10, "lease_ttl_s": 60}'
# → {"status":"ok","token":"...","lease_ttl_s":60}
curl -sX DELETE http://localhost:6389/v1/sessions/$sidSee the HTTP API docs. The OpenAPI 3.1 spec is also served live by the running server at GET /v1/openapi.json (auth-exempt) for codegen tools.
The wire format is three newline-terminated UTF-8 lines (command\nkey\narg\n). Useful for debugging or for languages without a ready-made client:
$ nc localhost 6388
l
my-key
10
ok abc123... 33
r
my-key
abc123...
okThe connection must stay open. By default, locks are auto-released on disconnect. See the protocol reference for every command.
Tokens are 32 lowercase hex chars: <16-hex monotonic prefix><16-hex random salt>. The prefix is a server-monotonic uint64 (big-endian) that strictly increases on every grant, so a token also serves as a fencing token. Compare two tokens for the same key lexicographically (or parse the prefix as a uint64): the larger token was issued later, so a downstream resource can store the most recent token it has seen and reject any write whose token compares less.
tok, _, _ := client.Acquire(c, "row:42", 5*time.Second)
fence, _ := client.FenceFromToken(tok) // uint64 — pass to your DB / blob storeFences are per-server-instance: each dflockd process is the source of truth for the keys clients route to it (CRC32 sharding). Caveats: tokens for different keys aren't meaningfully ordered against each other; a Limit>1 semaphore issues a distinct fence per grant, not per resource.
By default cross-restart monotonicity comes from seeding the counter with time.Now().UnixNano(), which holds as long as the wall clock doesn't regress across the restart (NTP step, VM snapshot, manual change). For strict monotonicity even across crashes and clock regressions, set --fence-state-file=/path: dflockd pre-allocates fence ranges to a checksummed two-slot journal (one fsync per ~1M grants — measured ~3 ns/op extra vs. the in-memory path, single-digit percent of token-mint cost), and the next instance always seeds above the highest value the prior one could ever have issued. The state file is tiny (up to 64 bytes), exclusive-locked (flock(2)) while dflockd is running; the flag is refused at startup on platforms without that lock (Windows and other non-Unix targets). Default off preserves the "single binary, zero deps" promise.
One acquire + release per operation over a persistent TCP connection. Median of three runs on v1.16.0; Apple M1 (MacBook Air, 8 GB RAM) with server and clients on localhost. Unique keys, no contention.
| Workers | Rounds | Ops | Throughput | Mean | p50 | p99 |
|---|---|---|---|---|---|---|
| 1 | 1,000 | 1,000 | 13,440 ops/s | 0.074 ms | 0.062 ms | 0.178 ms |
| 10 | 1,000 | 10,000 | 45,861 ops/s | 0.217 ms | 0.215 ms | 0.382 ms |
| 50 | 1,000 | 50,000 | 83,516 ops/s | 0.590 ms | 0.513 ms | 2.096 ms |
| 100 | 1,000 | 100,000 | 87,448 ops/s | 1.124 ms | 0.935 ms | 4.192 ms |
| 200 | 1,000 | 200,000 | 89,706 ops/s | 2.200 ms | 1.864 ms | 7.892 ms |
| 500 | 1,000 | 500,000 | 85,124 ops/s | 5.814 ms | 5.117 ms | 18.554 ms |
The HTTP transport adds ~2× latency vs. raw TCP at the same concurrency (extra round trip overhead, JSON encode/decode, middleware chain). Run your own benchmarks with go run ./cmd/bench --help.
CLI flags take precedence over environment variables. The full table is in the server docs; the most common knobs:
| Flag | Env | Default | Description |
|---|---|---|---|
--host |
DFLOCKD_HOST |
127.0.0.1 |
Bind address |
--port |
DFLOCKD_PORT |
6388 |
TCP port |
--http-port |
DFLOCKD_HTTP_PORT |
0 (disabled) |
HTTP REST port |
--default-lease-ttl |
DFLOCKD_DEFAULT_LEASE_TTL_S |
33 |
Default lease TTL (seconds) |
--max-locks |
DFLOCKD_MAX_LOCKS |
1024 |
Cluster-wide cap on unique keys |
--max-waiters |
DFLOCKD_MAX_WAITERS |
0 |
Per-key waiter cap (0 = unlimited) |
--max-connections-per-ip |
DFLOCKD_MAX_CONNECTIONS_PER_IP |
0 |
Per-IP TCP cap |
--http-rate-limit-per-ip |
DFLOCKD_HTTP_RATE_LIMIT_PER_IP |
0 |
Per-IP HTTP req/s |
--tls-cert / --tls-key |
DFLOCKD_TLS_CERT / _KEY |
(unset) | Enable TLS on both listeners |
--auth-token-file |
DFLOCKD_AUTH_TOKEN_FILE |
(unset) | Shared-secret auth token |
--auto-release-on-disconnect |
DFLOCKD_AUTO_RELEASE_ON_DISCONNECT |
true |
Release tokens on disconnect |
--fence-state-file |
DFLOCKD_FENCE_STATE_FILE |
(unset) | Path to fence-counter journal. Set for strict cross-restart fencing-token monotonicity (~3 ns/op extra). |
- Go (in-repo) —
go get github.com/mtingers/dflockd/client(docs) - Python — dflockd-client-py
- TypeScript — dflockd-client-ts
The TCP wire format is stable; client libraries hash keys with CRC-32 (IEEE) so the same key resolves to the same server in any language.
go test ./... # full suite
go test -race ./... # with the race detector
make complexity # per-function lines + cyclomatic complexity
make openapi-sync # mirror internal/httpapi/openapi.json → docs/dflockd v2 dropped the pub/sub / SSE layer that earlier releases shipped. The pre-refactor source is preserved under old/ (gitignored, build-tagged out) for reference. Current docs describe only the v2 surface.