Pinboard is the control plane between clients and workers.
Unauthenticated GET /healthz GET /readyz
Worker POST /registry/register POST /registry/heartbeat POST /registry/release POST /registry/tag POST /registry/deregister
Operator GET /registry/health GET /registry/workers GET /registry/overview GET /registry/stats
POST /registry/wind_down POST /registry/drain POST /registry/undrain GET /metrics (Node entry)
Pinboard → worker GET {advertiseUrl}/_pinned/challenge?challenge= GET {advertiseUrl}/_pinned/stats POST {advertiseUrl}{ns}/{id}/_wake/registry/*and/metrics:401 {"error":"unauthorized"}without a valid Passport credential.403 {"error":"forbidden"}for a credential missing the route's scope (pinboard:workerfor worker routes,pinboard:operatorfor the rest; principals) or carrying a malformedpinboardclaim.- Unknown
/registry/*paths authenticate aspinboard:workerand pass the serving check before answering404 {"error":"not found"}.
- Every
/registry/*route but/registry/healthanswers503 {"error":"not the serving fleet"}+Retry-After: 1while this node is not serving. - Bodies are JSON. A body over 1 MiB (8 MiB for register) is
413 {"error":"body too large"}. A body failing the route's schema is400 {"error":"expected {...}"}. Unparseable JSON is400 {"error":"invalid json"}. - Data plane: an id that is empty, over 256 characters, or outside
[A-Za-z0-9._~-]is400text/plain. A namespace is/plus one path segment. POST {advertiseUrl}{ns}/{id}/_wakeis the durability sweep's wake:200 {"ok":true}once the instance is resident,202 {"activating":true}while hydrating. Mounted only on a host with a durability store.
Leases and epochs
- Placement row: hash
{prefix}{{bucket}}:{env}:{ns}:placements(the bucket sits in literal hash-tag braces), fieldid, bucket = the first 8 hex digits ofsha256(id)modshards.- Fields:
sessionId,epoch?,expiry?,hostname?.epoch?is absent only on ownerless placeholder rows paged in by the sweep.expiry?is the wind-down deadline. The row counts as free past it.hostname?is the resurrection target on placeholder rows.
- Placed = row present, owner live, wind-down unexpired.
- Fields:
- Worker record: a hash field carrying
expiry(now + leaseTtlat register and each heartbeat, bounded by a timed drain). - Dead once
expiry ≤ now. - Pruned once
expiry + lease < now, together with bindings without a live worker: by the next register, by each mirror poll, and by the sweeper's GC. - Redis TTLs: the control queue, the sweeper lease, the fleet license, and the drain key of a timed drain (a
nullcordon has none). Membership is one hash with a per-memberexpiryfield. - Sessions: registration mints a
sessionId. Placements belong to the session, one session per placement. Placements pointing at a dead session are re-claimed on the next resolve. - Placement epoch: a decimal string, monotonic per bucket (
max(now-based, previous + 1, floor + 1)), minted at every claim.- It rides each forward as
Pinboard-Epoch, quotes releases, and fences overwritten-marker deletes. - A worker answers
400 malformed placement epochto a non-digit value.
- It rides each forward as
- Binding epochs (hostname-aware): an equal or lower epoch conflicts
409(register). - Fleet license:
{schema, shards, hostnameAware, epoch}in Redis, claimed with a TTL of one lease, overwritten by a higherepoch, and compared byte for byte.- Cluster members must match every live member's config unless that member's epoch is lower.
Worker protocol
POST /registry/register
→ { clientId, secret, namespaces: string[], advertiseUrl, headroom: number, draining: boolean,
challenge, challengeValue, hostname?, env?, envs?: {ns: env}, takeover?: "wind_down" | "deny",
epoch?: integer ≥ 0, instances?: {ns: id[]}, prevSessionId? }
← 200 { sessionId, expiresInMs, draining, drainDeadline?, duplicates?: [{ns, id}] }- Pinboard probes
GET {advertiseUrl}/_pinned/challenge?challenge=<challenge>withPinboard-Proxy-Secret: <secret>(timeoutverifyTimeoutMs, default 2500 ms) and requires{"value": <challengeValue>}.- Credential scope (
403below) is checked before the probe. - Every registration probe failure is
400 {"error":"registration probe failed"}. Probe details remain in host logs. Worker retry backoff remains active.
- Credential scope (
claims.pinboard.namespacesis required and nonempty for both registry roles;["*"]grants all namespaces. OptionalallowedPortslimits advertise URLs to integer ports 1–65535; omission permits every port withinallowedBackendHosts.403 {"error":"namespace not allowed: …" | "backend host not allowed: …" | "advertiseUrl port not allowed: …" | "hostname not allowed: …" | "env not allowed: …"}when the body falls outside the credential'spinboardclaims.409 {"error"}on a conflict:
| Conflict | Error string |
|---|---|
| Client id under another account | client belongs to another account |
Hostname-aware: (hostname, ns) bound to another env | hostname {h}{ns} is bound to env {e} at epoch {n}, not {e2} — register with a higher epoch to displace |
| Hostname-aware: env agreed another takeover | env {e} agreed takeover {t} at epoch {n}, not {t2} — register with a higher epoch to displace |
- A higher
epochrewrites the binding and quarantines new placements in the displaced(hostname, ns)for one lease. Ids with a placement row bypass the quarantine. - Field rules:
- Single mode rejects
hostname,env,envs,takeover, and a nonzeroepoch(400). - Hostname-aware mode requires
hostname;envdefaults tohostname. envskeys andinstanceskeys must be registered namespaces.advertiseUrlis an HTTP(S) URL with a hostname and without userinfo. Trailing slashes are stripped.hostnameandenvare lowercased.*, whitespace,/, or:in either is400 {"error":"invalid hostname: …" | "invalid env: …"}.takeoverdefaults towind_down.
- Single mode rejects
instancesreclaims ids still held in memory: rows owned byprevSessionIdre-bind to the new session keeping their epoch. Every other id returns induplicatesfor the worker to evict. A reclaimed row carrying a wind-down expiry re-queues itswind_downto the new session.expiresInMsis the lease TTL (PINBOARD_LEASE_TTL_MS, default 15000).drainingis the worker's own flag OR an operator drain.drainDeadlineis the remaining seconds of a timed operator drain.
POST /registry/heartbeat
→ { sessionId, headroom, draining, challenge, challengeValue, acks?: msgId[] }
← 200 { expiresInMs, draining, drainDeadline?, degraded?: true,
messages?: [{ msgId, type: "wind_down", ns, id, deadline: seconds | null }] }challengeandchallengeValueare both required. Pinboard re-probes the advertise URL each heartbeat.409 {"error", "reregister": true}:unknown session(unknown or expired)registration voided: challenge value mismatch on the advertise URL(the probe answered another worker's value; records dropped)
409 {"error", "superseded": true}:registration superseded: {hostname}{ns} now bound to env {env} at epoch {n}; records dropped.- The Python worker re-registers only from the heartbeat path, gives up on release and deregister after three tries, and retries a heartbeat
403with the same session. 403 {"error":"forbidden"}: the credential's account differs from the registering one, or its claims no longer cover the record.- A probe that is unreachable, non-2xx, or body-less still renews the lease and sets
degradedSinceon the record. A passing probe clears it. Only the heartbeat probe sets the flag.- While set: existing placements keep routing, new placements skip the worker, the ack carries
degraded: true.
- While set: existing placements keep routing, new placements skip the worker, the ack carries
messagesre-deliver on every ack untilacksnames theirmsgIds.deadlineis relative seconds at peek time,nullrevokes. The control queue expires two lease TTLs after its last write or peek.- The record's
headroomis replaced by the heartbeat's. Each new placement decrements the stored value until the next heartbeat.
POST /registry/release
→ [{ sessionId, namespace, id, epoch?: "digits" }] (non-empty)
← 200 { released: number }- Compare-and-delete: a row is freed only while its
sessionIdmatches and itsepochequals the quoted one (''quotes an epoch-less row). 409 {"error":"unknown session","reregister":true}for a session with no record.400 {"error":"invalid namespace" | "namespace not registered: …"}.403from another account or outside the credential's claims.- Release skips the expiry check heartbeat and deregister apply.
POST /registry/tag
→ { sessionId, namespace, id, epoch: "digits", tag } (tag: 1–256 chars)
← 200 { tagged: boolean }- Compare-and-set of the placement's owner tag: written only while the row's
sessionIdmatches and itsepochequals the quoted one;tagged: falseotherwise. - The tag survives register-time reclaim and wind-down and is dropped with the row on release or re-placement.
409 {"error":"unknown session","reregister":true}for a session with no record.400 {"error":"invalid namespace" | "namespace not registered: …"}.403from another account or outside the credential's claims.
POST /registry/deregister
→ { sessionId }, ← 200 {}.
- Removes the session from every namespace list and drops its record. Placements are left to dead-owner eviction.
- Unknown session:
409 {"error":"unknown session","reregister":true}. Another account's session:403.
Health
GET /healthz:200 {"ok":true}.GET /readyz: empty body.200when not draining and the node holds the fleet license and passes cluster membership, else503.- Cold-start quarantine shows only as
quarantineMson/registry/health.
- Cold-start quarantine shows only as
Operator endpoints
GET /registry/health:{ resolutions: {outcome: count}, connections, quarantineMs, uptimeMs, rssBytes? }for this node. It answers while booting or draining.- Outcomes:
forward,unknown-ns,no-backends,quarantine,not-serving,no-hostname-workers,binding-quarantine,takeover-pending,env-conflict,nack. quarantineMs: the largest remaining cold-start window across the buckets, rounded up.
- Outcomes:
GET /registry/workers:{ workers: [{ sessionId, clientId, advertiseUrl, namespaces, hostname, envs, takeover, headroom, draining, degradedSince?, expiry }] }.- Live records with
secretandaccountstripped, namespaces filtered to the credential's claims, sorted by hostname then session.
- Live records with
GET /registry/overview, scoped like/registry/workers:{ workers, bindings: { hostnames: {"{hostname}{ns}": env}, envs: {env: takeover} }, namespaces: [{ hostname, env, namespace, sessionIds, entries: [{ id, sessionId, expiry?, tag? }] }] }- Query filters, ANDed:
ns(exact namespace,400 {"error":"invalid namespace"}when malformed),prefix(id prefix),tag(exact owner tag; untagged rows are excluded,400 {"error":"empty tag"}when blank).nsdrops other namespaces fromnamespaces;prefixandtagfilterentries. Live placements only. GET /registry/overview?ns=/threads&prefix=H~&tag=ws:Wlists one workspace's placements under one harness.
GET /registry/stats:{ workers: [{ sessionId, clientId, hostname, stats } | { …, error: "unreachable" }] }.- Each live worker's
GET {advertiseUrl}/_pinned/stats, attested, 3 s timeout, 8 concurrent. - Namespaces and instances are filtered to registered ones.
- Each live worker's
POST /registry/wind_down{ namespace, id?, deadline: seconds | null, env? }→{ queued: boolean | number }.- Stamps the placement's wind-down expiry at
now + max(deadline * 1000, leaseTtlMs)(nullclears) and queues awind_downcontrol message to the owner. The fence survives worker death and re-notifies a replacement owner. - Without
id, every placement in the(env, namespace),queuedthe count. 400 {"error":"env is required in hostname-aware mode" | "env requires hostname-aware mode"}whenenvis absent in hostname-aware mode or present otherwise.- Scope is judged on the owner record's
hostnameandenvs[namespace], or on the row'shostnamewhen the owner record is gone. - Out of scope:
403 {"error":"namespace not allowed: …" | "env not allowed: …" | "forbidden"}.
- Stamps the placement's wind-down expiry at
POST /registry/drain{ clientId, deadline: seconds | null }→{ drained: true }.- Writes the client's drain config:
nullcordons, a number forces release atnow + max(deadline * 1000, leaseTtlMs). A timed drain key lives 7 days past its deadline. 403 {"error":"forbidden"}for an unknown or out-of-scopeclientId.
- Writes the client's drain config:
POST /registry/undrain{ clientId }→{ revoked: boolean }. Unknown or out-of-scopeclientId:403.- Account ownership is checked by register, heartbeat, release, and deregister only. Wind-down, drain, undrain, and the read routes judge the credential's namespace, hostname, and env claims.
- Recipes: wind-down and drain, operator drain.
GET /metrics: Prometheus text. Node entry only, outsidebasePath, present only with a metrics registry configured (otherwise404 {"error":"unknown namespace"}).403 {"error":"forbidden"}unless the operator policy carries"*"innamespacesand unrestrictedhostnames/envs.500 {"error":"metrics collection failed"}on a snapshot failure.- The fleet overview behind the collectors is memoized for 5 s.
Placement
The resolve script runs atomically per (env, namespace, id):
- An existing row forwards to its owner when the owner is live (record present,
expiry > now) and its wind-down has not passed.- A row whose owner's
hostnamediffers from the request's applies the env's takeover:deny→takeover-denied;wind_down→ the row is fenced atnow + leaseTtl(takeover-pendinguntil then).
- A row whose owner's
- No
ns_workerslist for{hostname}{ns}→unknown-ns(404 {"error":"unknown namespace"}).- In hostname-aware mode the binding lookup runs first and can refuse
no-hostname-workers,binding-quarantine, orenv-conflictbefore the script. - A script
no-backendsthere reports asno-hostname-workers.
- In hostname-aware mode the binding lookup runs first and can refuse
boot_timeisSET NXto now, per bucket. Whileboot_time + leaseTtl > nowthe bucket's result isquarantine.- Candidates: sessions in the namespace list that are live, not draining, not degraded, not the excluded session, with
expiry - now ≥ min(5000, leaseTtl / 3).- Outlier-ejected sessions are used only when no other candidate remains.
- None →
no-backends.
- Affinity: a caller-attested request prefers its own session. Otherwise
affinity.key(req, client), or the client-IP key pertrustedProxieswhen unset, picks a stable candidate by rendezvous hash. A customnullkey skips the preference.- The preference is honored while that worker's
headroom ≥ mean / balanceFactor.
- The preference is honored while that worker's
- Otherwise power-of-two: two random candidates, the higher
headroomwins. - A new epoch is minted. The old row (if any) is deleted and the id listed on the overwritten hash. The chosen worker's stored headroom is decremented. The row
{ sessionId, epoch, expiry? }is written withHSET.
Refusals are 503 {"error"} + Retry-After: max(1, ceil(remaining / 1000)) seconds:
| Error string | When | Retry-After |
|---|---|---|
no live backends for namespace | no-backends | 1 |
registry cold start, retry | quarantine | remaining window |
not the serving fleet | not-serving | 1 |
no live workers for hostname | no-hostname-workers | 1 |
hostname binding displaced, retry | binding-quarantine | remaining window |
hostname bindings inconsistent, retry | env-conflict | 1 |
takeover in progress, retry | takeover-pending | its fence |
placement held by another hostname | takeover-denied | 1 |
worker inflight cap exceeded, retry | past forwards.maxInflight per worker (default 1024) | 1 |
- Namespace lists and worker records are read per resolve. Nothing is cached. Heartbeat re-adds the worker to every namespace list in its bucket, and a bucket missing a namespace list copies the previous bucket's.
- Route misses:
404 {"error":"unknown namespace"}for a request path,404 {"error":"not found"}for an upgrade or a path outsidebasePath. A handler throw is500 {"error":"internal error"}. - Hostname-aware mode: the
Hostheader (port stripped, lowercased) names the hostname (400 {"error":"missing Host header"}without one). The(hostname, ns)binding names the env, the env binding the takeover.
In-band refusal and re-placement
- A worker refusing a new activation answers
503withRetry-After: 1andPinned-Refusal: lease-unhealthy. Pinboard:- strips the header, releases the placement, counts
nack; - re-resolves once excluding that session (upgrades and body-less requests only; a request with a body gets the 503 at once);
- answers
503 {"error":"worker refused activation, retry"}when the retry also refuses or no other worker remains; - answers with that refusal's string on a refusal from the re-resolve,
404 {"error":"unknown namespace"}when the namespace vanished meanwhile.
- strips the header, releases the placement, counts
- A resolution older than 5 s by the time the forward sends is re-resolved, twice at most; the worker's epoch guard catches a forward that is still stale.
- A fresh placement whose forward never reached TCP is released so the client's retry re-places. An unreachable forward never marks the record degraded.
- Forward failures:
502 bad gateway(text) for an unreachable worker,504 upstream timeoutafterforwards.timeoutMs(default 30000), an empty502when the client aborted before the forward started. - A 2xx from an HTTP forward (upgrades excluded) on an overwritten-listed id clears the marker at or below the served epoch.
Attestation and headers
Every inbound Pinboard-* header is stripped before forwarding. X-Forwarded-For is replaced with the observed peer address. Hop-by-hop headers are dropped. Responses lose Pinboard-* and hop-by-hop headers and gain X-Accel-Buffering: no.
Pinboard-Attestation: set on every forwarded request,_wake, and_pinned/statscall. Compact JWS, header{"alg":"HS256","typ":"pinboard+jwt"}, signed with the worker's registeredsecret.- Claims:
aud= the worker'ssessionId(in-process cluster calls use the worker's local audience),iat,exp=iat + 30,caller?({ns}/{id}of an attested calling instance),depth?,acct?. acct?={ sub, scopes?, claims }of the data-plane credential withpinboard:*scopes and thepinboardclaim removed.- The worker verifies with 30 s clock skew and answers
403 {"detail":"invalid attestation"}or WebSocket close1008._pinned/challengealso acceptsPinboard-Proxy-Secret.
- Claims:
Pinboard-Epoch: the placement epoch of the resolved row, on forwards and_wake.Pinboard-Proxy-Secret: the worker's secret, only on the_pinned/challengeprobe.- The worker answers
{"value": …},404 unknown challengefor a stale challenge,403 invalid proxy secretwithout the secret or a valid attestation.
- The worker answers
- Caller attestation, inbound from an instance:
Pinboard-From: {ns}/{id},Pinboard-Session,Pinboard-Session-Secret,Pinboard-Subrequest-Depth(≤ 9 digits).- Verified against the session's secret and the caller's placement row.
- Failures are
403 {"error"}:caller attestation requires session credentials,unknown session,invalid session secret,malformed caller claim,caller placement mismatch,malformed subrequest depth. - A depth over 50 is
508 {"error":"subrequest depth exceeded"}.
- A verified caller bypasses passport; the target must lie inside the caller's registration (its hostname and
envs[ns]), else403 {"error":"target outside caller scope"}. - Data-plane passport runs only when
namespacesrules are configured.- Failures:
401 {"error":"invalid credential" | "authentication required"},403 {"error":"namespace not allowed" | "insufficient scope"}.
- Failures: