TCP · Go standard library · one process

A lightweight TCP server for named locks

— a distributed mutex without the distributed system.

Terminal:7070
$ docker run -p 7070:7070 \
   ghcr.io/monolock-dev/monolock
1
static binary
0
dependencies, no database
0
config files to write
The whole idea

One connection,
one lock

A TCP connection is one claim on one named lock. Closing the connection is the release — there is no release call to forget, and the next waiter in the FIFO queue is promoted at once.

Promotion is a push, not a poll: the server sends ACQUIRED on its own initiative, so handover latency is one one-way trip.

Read the mental model →
locknightly-import
token41
holderworker-alease 2sACQUIRED
#1worker-blease 2sWAITING
#2worker-clease 5sWAITING
A connection is a claim. Close it, hang it, or queue another claimant.
Owner's exitHandover latency
Graceful (connection closed)immediate
Force-released by an adminimmediate
Process killed, socket reset by the OSimmediate
Hang, GC pause, network partition≤ the owner's lease
Slow reader/writer (stuck socket)≤ io-timeout per operation
In your code

Taking a lock

go get github.com/monolock-dev/monolock-go
worker.goGo client
import monolock "github.com/monolock-dev/monolock-go"

c := monolock.New(monolock.Config{Address: "127.0.0.1:7070"})

err := c.Do(ctx, "nightly-import", 2*time.Second,
    func(ctx context.Context, token uint64) error {
        for {
            select {
            case <-ctx.Done():
                return nil // the lock is gone; Do reports why
            case job := <-jobs:
                if err := process(ctx, job, token); err != nil {
                    return err
                }
            }
        }
    })
Do
Blocks until this process owns the lock, runs your function for exactly as long as the server keeps confirming ownership, and releases it on the way out.
token
The fencing token of this grant. Hand it to the resource the lock guards with every write, and have the resource reject anything smaller.
ctx
Cancelled the moment ownership stops being confirmed. Ownership ends, work must stop.
DoRetry
Same, with exponential backoff and jitter built in. Go client docs →

This design removes a whole class of client bugs.

No release call to forget
The lock is released whenever your function returns — or panics.
No session id to persist and lose
The connection is the session. Nothing to store, nothing to leak.
No lock without a live process
If the process dies, the kernel closes the socket, and the lock moves on.
01

Fencing tokens built in

Every grant carries a monotonically growing token. Pass it to the resource the lock guards and stale holders fence themselves out — safety on top of liveness.

Fencing tokens →
02

Client-chosen leases

Each client picks its own failure-detection window per connection. Heartbeats are RTT-aware, and a dead holder is detected within one lease — while a healthy one can hold forever.

Leases & heartbeats →
03

No dependencies, no database

A single static binary built on the Go standard library alone. No config file, no storage, no cluster to babysit — one process is the whole deployment.

Deployment →
How it compares

Including the rows monolock loses

Honest side-by-side comparisons with the locks you already know.

monolockRedis / RedlockPostgres advisoryetcd
Ownershipconnection-scopedTTL key, renewedsession-scopedlease + keepalive
Releaseclosing the connectionexplicit, owner-checkedunlock or disconnectexplicit, lease revoke
Fencing tokenevery grantnone — the Redlock debatenonerevision numbers
WaitersFIFO queue, pushedretry loops, no orderblocking waitwatch on a prefix
Survives losing its nodeno replicationwith replicas, contestedwith HA Postgres✓ quorum
What you operateone static binary, one porta server — or five for Redlocka database, and its HA setupa 3–5 node cluster to keep healthy
Simple by design

What you don't get

A single point of coordination — no replication, no quorum, no consensus. Know exactly what you get, and what you don't.

capacity
One connection is one file descriptor, so capacity is whatever RLIMIT_NOFILE allows. No connection limit, no waiter-queue limit.
at the limit
The server backs off and retries rather than exiting, and picks connections up as sessions free descriptors.
state
Per-session state is small and fixed; there is no per-lock history and nothing is persisted.
clocks
Only durations cross the wire, never timestamps. Both sides use monotonic time, so clocks need no synchronisation.

One process, one port, no setup

# run the server
$ docker run -p 7070:7070 ghcr.io/monolock-dev/monolock
# add the client
$ go get github.com/monolock-dev/monolock-go