Concepts
Resources, contexts, and the per-context reputation cell: how score, cooldown, recovery, and the blocklist fit together.
Resource
A resource is one interchangeable member of your pool, identified by a kind and a value:
kind—PROXY,ACCOUNT, orSESSION.value— an opaque string that you choose and that means something to you: a proxy endpoint, an account id, a session handle. The pool never parses it.
The pair is the identity. PROXY/proxy-1 and ACCOUNT/proxy-1 are two different resources. Registering is idempotent, so calling Register on every worker boot is the normal pattern.
Context
A context is a string naming what the resource is being used for — checkout-us, search-eu, login-jp. You pass it to Acquire and to Report, and it is the second half of every reputation lookup.
Contexts are free-form and created on first use; there is no registration step. Choose them so that one context is one thing that can burn a resource independently — usually a destination, a tenant of yours, or a workload. Too coarse (default for everything) and one bad destination benches a resource for all of them. Too fine (one per request) and no cell ever accumulates enough history to be useful, plus every new cell counts against the pool's cell budget.
ReputationCell — the (resource × context) pair
Reputation is not stored per resource. It is stored per resource × context pair, and that pair is called a reputation cell. One resource holds as many cells as the contexts it has been used in, each with its own score, its own streaks, and its own cooldown.
| Field | Meaning |
|---|---|
score | Continuous reputation in [-100, 100], starting at 0.0. Moves on every reported outcome and weights selection. |
state | HEALTHY · COOLING · RECOVERING · BLOCKLISTED — the gate that decides whether this cell can be handed out. |
consecutiveFailures | Failure streak. Reaching the cool threshold is what starts a cooldown. |
consecutiveSuccesses | Success streak. Reaching the recover threshold is what ends probation. |
windowSize | How many recent outcomes are retained (10 by default). The streak counters are unbounded running values; the window is the bounded recent history. |
cooldownUntil | When the current cooldown expires, or null when the cell is not cooling. |
updatedAt | When the last outcome was applied to this cell. |
A cell is created by Report, never by Acquire. That is why a freshly registered resource shows contexts: 0 in the dashboard until you report on it: Acquire scores a candidate against a transient, never-persisted fresh cell, so a resource nobody has reported on is treated as neutral and HEALTHY — new resources are selectable immediately rather than having to earn their way in.
Why per context is the whole point
Take one proxy and two destinations. The proxy gets hard-blocked by the checkout endpoint but keeps working perfectly against search. With reputation stored per resource, that single block is a fact about the proxy, so you either lose a working proxy for search or keep feeding a burned proxy to checkout. Both are wrong, and which one you get depends on a threshold you had to guess.
PROXY proxy-1.example.net:8080
├─ context "checkout-us" score -34.0 state COOLING cooldownUntil 10:41:02
└─ context "search-eu" score 35.0 state HEALTHY cooldownUntil null
Acquire("search-eu") → may return proxy-1 (its search-eu cell is healthy)
Acquire("checkout-us") → will not return it (its checkout-us cell is cooling)With cells, the block is a fact about this proxy in this context. The proxy keeps serving search at full throughput while checkout routes around it, and it re-earns checkout on its own. Nothing in your code has to model that — you only ever report per context, and the isolation falls out of the key.
The four states
| State | Selectable? | Meaning |
|---|---|---|
HEALTHY | Yes | Trusted. The normal state. |
COOLING | No | Benched until its cooldown expires. |
RECOVERING | Yes | On probation — handed out again, but still proving itself. |
BLOCKLISTED | No | Isolated until explicitly released. Never left by traffic alone. |
consecutiveFailures >= coolAfter (2)
┌───────────┐ ───────────────────────────────────▶ ┌───────────┐
│ HEALTHY │ │ COOLING │
└───────────┘ ◀─────────────────────┐ └───────────┘
▲ │ │
│ consecutiveSuccesses │ │ cooldown expired,
│ >= recoverAfter (2) │ │ then one success
│ ┌────────────┐ ◀───────────┘
└────────────────────── │ RECOVERING │
└────────────┘
BLOCKLISTED is reachable from any state via an operator block, and is left
only by unblock or block expiry — never by reported outcomes.Cooling: what a failure actually does
Every reported failure lowers the score and increments the failure streak. Only when the streak reaches the cool threshold (2 by default) does the cell move to COOLING — a single blip does not bench a healthy resource. The penalty and the cooldown length both depend on the failure type you report:
| FailureType | Score penalty | Base cooldown | Use it for |
|---|---|---|---|
BLOCKED | 30 | 1 h | An active block: 403, captcha wall, ban page. |
TLS_HANDSHAKE | 15 | 5 min | TLS negotiation failed — often interception or a dead endpoint. |
CONNECTION_RESET | 10 | 2 min | Connection dropped mid-flight. |
TIMEOUT | 5 | 1 min | No response in time. |
SLOW | 2 | 30 s | Completed, but too slowly to be worth using. |
The cooldown is that base doubled for each consecutive failure, capped at 64× — base(type) × 2^min(consecutiveFailures - 1, 6). So a proxy that keeps getting blocked goes 1 h, 2 h, 4 h… rather than being retried every hour forever, while a merely slow one starts at 30 s.
While a cooldown is still running, further failures keep moving the score but do not restart the cooldown or re-emit the cooling event — a late-arriving result belongs to the incident already being punished, not to a new one. That is what stops one bad minute from turning into an exponentially growing bench.
Recovery: how a cell earns its way back
Recovery is success-driven, not just time-driven. Once the cooldown has expired, the cell is not silently healthy again — the next reported success moves it to RECOVERING, and the success streak restarts from that moment, so successes you happened to report while it was still cooling cannot shortcut probation. After recoverAfter consecutive successes (2 by default) it is promoted back to HEALTHY and a ResourceRecovered event is emitted.
RECOVERING is selectable, which is the point: probation means "back in rotation, being watched", not "waiting on the bench". A single failure during probation resets the success streak and, at the cool threshold, benches it again.
Score and how selection uses it
Score is a continuous value in [-100, 100], clamped at both ends. A success adds 5; a failure subtracts the penalty for its type. It is a separate signal from state: state decides whether a cell can be handed out, score decides how likely it is to be chosen among those that can.
Selection is a score-weighted random choice, not "always the best". Each eligible candidate's weight is (score − lowestScoreAmongCandidates) + 1.0, so higher scores win more often but every eligible candidate keeps a nonzero chance. Two things follow, both deliberate: load spreads across the healthy pool instead of hammering the single best resource, and the weakest eligible candidate still gets an occasional turn — which is how a resource on the edge of recovery gets re-probed instead of starving forever.
Blocklist
The blocklist is the operator override, and unlike everything above it is keyed by resource, not by cell. Blocking isolates a resource from selection in every context at once; the engine will never put it into or take it out of that set on its own.
- Temporary —
POST …/block?seconds=3600. Expires on its own. - Permanent —
POST …/block?permanent=true. Released only by an explicit unblock. - Release —
DELETE …/block, which emitsRESOURCE_UNBLOCKED.
A block also beats an in-flight acquire: if a resource is blocked between being picked and the lease being claimed, the claim is undone rather than honoured — a block call that has already returned can never be bypassed. And Renew refuses to extend a lease on a blocklisted resource, so an existing hold lapses at its TTL instead of being kept alive.
Because BLOCKLISTED is terminal for the engine, reported outcomes on a blocklisted resource still move the score and window — evidence for a later release decision — but cannot change its state.
Leases
Acquire does not just recommend a resource, it leases it: an exclusive hold for one context, valid for 30 seconds by default. While a resource is leased, no other Acquire will hand it out — even for a different context — so two workers never share one proxy by accident.
Renewextends the lease by another TTL. Use it when your work outlives the window.Releasehands the resource back immediately. Not required for correctness — expiry is the safety net — but releasing promptly is what keeps the pool busy instead of waiting out TTLs.- The lease carries a monotonically increasing fencing token.
RenewandReleaseact only for the current holder, so a worker whose lease already expired and was re-acquired by someone else cannot disturb the new lease.
Leases are runtime coordination, not durable state: they are not included in the pool snapshot, so nothing is held immediately after a restart.
Events
Every decision above is emitted as an event, both to the live gRPC SubscribeEvents stream and to the durable audit trail that GET /api/events reads. The event types you will see: RESOURCE_LEASED, LEASE_RELEASED, RESOURCE_COOLED (with the causing failure type and the cooldown end), RESOURCE_RECOVERED, RESOURCE_BLOCKLISTED, RESOURCE_UNBLOCKED. An acquire that found nothing eligible is reported on the live stream as AcquisitionRejected.
Defaults in one place
| Knob | Default | What it controls |
|---|---|---|
windowSize | 10 | Recent outcomes retained per cell. |
coolAfter | 2 | Consecutive failures before cooling. |
recoverAfter | 2 | Consecutive successes to leave probation. |
| lease TTL | 30 s | How long an acquired lease stays valid. |
| cooldown backoff cap | 64× base | Ceiling on exponential cooldown growth. |
| score range | −100 … 100 | Clamp on the continuous reputation value. |
These are the hosted deployment's values, and they mirror the engine's reference defaults. Every rule on this page — the penalties, the backoff curve, the transition conditions, the selection weights — is implemented in the open-source engine at PreAgile/reputation-pool, so you can read the source rather than take this page's word for it. Next: Authentication.