Skip to content

Any path that is not a probe or /registry/* is an instance request. Pinboard reads the namespace and id from the path, finds or picks the one worker that owns the id, and proxies the request to it.

https://pinboard.example.com/threads/t_8f2a/stream?x=1
                            └──┬───┘└─┬──┘└──┬──┘└─┬┘
                          namespace   id    rest  query
→ {advertiseUrl}/threads/t_8f2a/stream?x=1

Addressing

PartRule
Namespace/ plus the first path segment. Must match a namespace some worker registered.
IdThe second segment. 1–256 characters of [A-Za-z0-9._~-]. Not decoded: % is rejected.
RestEverything after the id, forwarded as-is.
QueryForwarded as-is.

Ids are names the client chooses. The first request for an id places it; later requests reach the same worker until the placement ends.

Resolution

Each request runs one atomic resolve against Redis:

  1. Placed and owner live: forward to the owner.
  2. No worker ever registered the namespace: 404 unknown namespace.
  3. Bucket in cold start: 503 registry cold start, retry. A bucket is the id's Redis shard; it refuses new placements for one lease (15 s by default) after first use, so a wiped Redis cannot double-place ids held in memory.
  4. Pick a worker among the namespace's live workers that are not draining, not degraded, and have at least min(5 s, lease / 3) of lease left.
    • A caller instance prefers its own worker; otherwise the client IP (or a custom affinity.key) prefers one worker by rendezvous hash.
    • The preference holds while that worker's headroom is at least the mean divided by balanceFactor (1.25).
    • Otherwise two random candidates; the higher headroom wins.
    • None placeable: 503 no live backends for namespace.
  5. Mint a placement epoch and write the row.

A placement ends when its owner's lease lapses, the owner releases it, a wind-down deadline passes, or the worker refuses the activation.

Placement epoch

Every placement row carries an epoch: a decimal string, increasing per bucket, minted whenever the id is placed. It is sent on every forward as Pinboard-Epoch. Workers use it to fence writes: an activation holding an older epoch than the stored one has lost the id.

WhereUse
Pinboard-Epoch request headerThe epoch the forward resolved against
Release epochCompare-and-delete: frees the row only at that epoch
Tag epochCompare-and-set of the owner tag

Retries Pinboard makes

SituationPinboard does
More than 5 s passed between resolve and sendRe-resolves, at most twice
Worker answers with Pinned-Refusal: lease-unhealthyReleases the placement and, for requests without a body and for WebSocket upgrades, re-places once on another worker. Requests with a body get 503 worker refused activation, retry.
A new placement's worker is unreachable (TCP connect failed)Releases the placement so the client's retry re-places. Answers 502.

Pinboard never replays a request body. Every other failure goes back to the client.

Hostname-aware mode

With PINBOARD_HOSTNAME_AWARE=1 one fleet serves several hostnames, each with its own workers and data.

  • The Host header (lowercased, port stripped) names the hostname. Missing: 400 missing Host header.
  • Workers register a hostname and an env per namespace. The (hostname, namespace) binding names the env; placements live per (env, namespace).
  • An id placed by a worker on another hostname of the same env follows the env's takeover:
    • wind_down: the old owner gets a wind-down for one lease; requests answer 503 takeover in progress, retry until it lapses.
    • deny: 503 placement held by another hostname.
  • After a higher-epoch registration rebinds (hostname, namespace) to a new env, new ids wait one lease: 503 hostname binding displaced, retry.

Pages