-
Notifications
You must be signed in to change notification settings - Fork 0
HTTP API
Base URL http://127.0.0.1:7010. Interactive documentation at /docs, generated
from the same definitions the server uses, so it cannot drift.
| Method | Path | Token | Purpose |
|---|---|---|---|
GET |
/health |
none | Liveness, and which warden this is |
GET |
/v1/pool |
either | Pool size, allocated, free, reserved |
GET |
/v1/services |
either | List registrations, filter by project and kind
|
POST |
/v1/services |
API | Register a service |
GET |
/v1/services/{name} |
either | Look one up |
POST |
/v1/services/{name}/heartbeat |
API | Extend a lease |
DELETE |
/v1/services/{name} |
API | Release a port |
GET |
/v1/listeners |
either | Every socket bound on that machine |
DELETE |
/v1/listeners/{pid} |
API | Stop a process, off unless WARDEN_ALLOW_KILL
|
POST |
/v1/nodes |
cluster | A warden announces itself |
GET |
/v1/nodes |
either | Every warden this one knows |
DELETE |
/v1/nodes/{name} |
API | Forget a warden |
GET |
/v1/fleet/services |
either | Everything the fleet holds, plus what did not answer |
GET |
/v1/fleet/services/{node}/{name} |
either | One service on one named node |
GET |
/v1/update |
either | Whether a newer warden exists |
POST |
/v1/update |
either | Ask this warden to update itself |
POST |
/v1/fleet/update |
API | Ask every warden to update itself |
"API" means WARDEN_TOKEN, "cluster" means WARDEN_CLUSTER_TOKEN, "either"
accepts both. A check is skipped entirely when the corresponding setting is
empty. The cluster token opens announcing and reading and nothing that changes
state — see Cluster.
Authorization: Bearer <token>
curl -s localhost:7010/v1/services \
-H 'content-type: application/json' \
-d '{"name": "shop-api", "kind": "backend", "project": "shop"}'{
"name": "shop-api",
"kind": "backend",
"project": "shop",
"host": "127.0.0.1",
"port": 8000,
"pid": null,
"meta": {},
"ttl": null,
"created_at": "2026-08-31T12:00:00Z",
"updated_at": "2026-08-31T12:00:00Z",
"expires_at": null
}201 when the name is new, 200 when an existing registration was renewed. That
difference is the only way to tell a first start from a restart.
Fields you may send:
| Field | Meaning |
|---|---|
name |
Required. Unique on this warden |
kind |
Required. What the service is |
project |
Groups services of one codebase |
host |
Interface the service will bind to, default 127.0.0.1
|
preferred_port |
Wish for this port, fall back to the pool |
require_port |
Insist on this port, 409 if it is not free |
pid |
Process id, shown in listings |
ttl |
Seconds until the registration expires |
meta |
Free-form string map, stored and returned untouched |
Unknown fields are rejected with 422 rather than ignored, so a typo in
prefered_port is a loud failure instead of a silently different port.
- A registration that already exists keeps its port, unless another registration has taken it meanwhile.
-
require_portis granted if free, refused with409if not. -
preferred_portis granted if free, and otherwise quietly gives way to the pool. - Otherwise the lowest free port in the pool wins.
- Before a fresh port is handed out it is tested for an existing listener, so anything started outside the registry is skipped. A service keeping its own port is not probed, since it may still be bound to it.
Ports are tracked per host, so 10.0.0.5:8000 and 127.0.0.1:8000 are two
different endpoints and may be held by two different services.
ttl makes a registration expire. POST /v1/services/{name}/heartbeat pushes
the expiry out again:
curl -X POST localhost:7010/v1/services/ci-runner/heartbeat \
-H 'content-type: application/json' -d '{"ttl": 600}'Sent without a ttl, the heartbeat renews the lease the service registered with.
It can never turn a lease into a permanent registration by accident.
Expired registrations are dropped on the next request that touches the registry, not by a timer.
curl -s http://hub:7010/v1/nodes \
-H 'content-type: application/json' \
-H 'Authorization: Bearer <cluster token>' \
-d '{"name": "build-01", "url": "http://build-01:7010",
"pool_start": 9000, "pool_end": 9099, "version": "0.1.0"}'{
"name": "build-01",
"url": "http://build-01:7010",
"pool_start": 9000,
"pool_end": 9099,
"version": "0.1.0",
"first_seen": "2026-08-31T12:00:00Z",
"last_seen": "2026-08-31T12:04:00Z",
"expires_at": "2026-08-31T12:05:30Z",
"status": "online"
}status is computed as you ask, not stored, so it is never out of date.
You do not normally call this by hand: a warden with WARDEN_UPSTREAM set does
it for you, three times per lease.
{
"status": "ok",
"version": "0.1.0",
"node": "build-01",
"role": "edge",
"services": 2,
"nodes": 0
}role is edge when this warden reports to another, hub when it does not.
/health never requires a token, so it works as a container or load-balancer
probe.
Every refusal comes back as {"detail": "..."} with a status that says what
kind of problem it is:
| Status | Means |
|---|---|
401 |
Missing or wrong token |
403 |
Allowed to ask, not allowed to do — stopping processes with WARDEN_ALLOW_KILL off, or a process this account may not touch |
404 |
No such service, node or process |
409 |
A required port is held by someone else, or a process ignored the request to stop |
422 |
The request body does not hold up: a bad name, an unknown field, both port wishes at once |
503 |
The pool has no free port left |
The detail is written to be shown to a person as-is:
{"detail": "port 8000 is held by 'shop-api'"}Everything above is wrapped, with the same errors as exceptions:
from warden import WardenClient
with WardenClient("http://hub:7010", token="...") as client:
for node in client.nodes():
print(node.name, node.status)| Method | Does |
|---|---|
register(name, kind=..., ...) |
Claim a port |
lookup(name) |
One registration |
services(project=..., kind=...) |
All of them |
heartbeat(name, ttl=...) |
Extend a lease |
release(name) |
Give a port back |
pool() |
Pool usage |
listeners(udp=...) |
Sockets on that machine |
stop(pid, force=...) |
End a process |
nodes() |
The fleet |
announce(name, url=..., ...) |
Report a node |
forget(name) |
Remove a node |
fleet_services(project=..., kind=...) |
Everything the fleet holds |
fleet_lookup(node, name) |
One service on one named node |
session(name, ...) |
Context manager that releases on exit |
Exceptions all descend from WardenError: UnknownServiceError,
UnknownNodeError, PortUnavailableError, PoolExhaustedError,
NotPermittedError, ProtectedProcessError, StillRunningError,
UnknownProcessError. A warden that cannot be reached raises the base
WardenError with an explanation, not a bare connection error.
warden — nothing binds a port without asking ·
uv tool install warden-ports
Repository · Issues · Releases · PyPI · MIT
Getting started
While it runs
Several machines
Reference