Gateway host-health deadlock: mass quarantine → all hosts stuck as no-winner/suspicious #1506
Summary
I managed to get all hosts into a kind of deadlock on testnet.
How everyone entered quarantine
I created an escrow with the wrong owner (CLI key ≠ DEVSHARD_PRIVATE_KEY the gateway signs with), registered it on the gateway, and sent chat. The gateway broadcast / raced the request across participants. Every host correctly rejected with HTTP 403 (restricted to escrow owner). The gateway treated each 403 as a host health signal and applied a ~30 min inference quarantine to each host that answered.
I understand there are multiple ways to enter quarantine (404 session missing, transport failures, empty streams, etc.). On a small testnet this was easy to trip for the whole set at once; on mainnet that mass fan-out should be harder, but the same mechanism still exists.
How they got stuck after unquarantine
After I cleared quarantine via POST /v1/admin/participants/unquarantine, every host moved into suspicious / no-winner probation (failure_strikes left after quarantine; attempts marked primary_no_winner / suspicious_winner_deferred).
They never got out, because there were no non-suspicious winners left who could serve the client. Probation only clears when finished good attempts call ObserveSuccessfulInference (strike decrement). With every usable host on probation:
- Hosts still run inference and return real content
- Content is deferred (
suspicious_winner_deferred,winner_nonce=0) — not crowned to the client - Request fails / times out
- Strike recovery does not progress in practice when no clean host can complete the request for the client
So: wrong-owner 403 → mass quarantine → mass unquarantine into probation → no clean crown path → permanent suspicious deadlock.
Observed on
- Host: testnet gateway
.79 - Image:
ghcr.io/gonka-ai/devshard-gateway:testnet-v4-local(built fromdevshard-0.2.14-v4) - Log signals:
participant_limit_activated status=403,decision=primary_suspicious,primary_no_winner_reason=probation,suspicious_winner_deferred,picker_exhausted
Suggested product fixes
- Do not quarantine participants on 403 “restricted to escrow owner” (misconfigured escrow / signer mismatch, not a bad host).
- When no clean host remains, crown a suspicious winner that already returned good content (or accept deferred completions toward strike recovery).
- Add an admin “forget / fully clear” path that lifts quarantine without leaving the host in no-winner probation (separate from soft unquarantine).
Ops workaround that clears the stuck state
Restart the gateway after throttle rows are cleared (in-memory probation dies with the process). Soft unquarantine alone is not enough — it re-enters probation by design.
Other ways to reach the same mass-quarantine shape
Same pattern every time: one shared gateway/request fault × redundancy race × immediate quarantine = whole participant set dirty. Then unquarantine (or natural expiry) → everyone on no-winner probation → no clean crown path.
Immediate mass quarantine (one bad chat can hit everyone)
| Trigger | Duration | Shared cause that fans out |
|---|---|---|
403 Forbidden on /chat/completions |
~30m | Wrong escrow owner (this incident); other host auth rejects that return 403 on inference |
| 404 Not Found on inference | ~30m | Dead/missing session on hosts (session not found), escrow never registered, hosts wiped session storage while the gateway still routes it |
401 + timestamp drift |
~30m | Gateway clock skew vs hosts, or signed requests aged in a long queue before send (>30s) |
| 429 / 503 | ~60m | All hosts overloaded (PoC / capacity / load); every raced host reports throttle |
| Non-EOF transport failure | ~30m | Gateway cannot reach anyone (bad participant URLs, network cut, TLS/dial failures across the set) |
These are the dangerous ones: one attempt per host → quarantine, no strike streak required.
Slower / less “one-shot” mass quarantine
| Trigger | Needs | Mass risk |
|---|---|---|
| Empty stream | 3 strikes → shadow quarantine | Needs repeated empty finishes; empties are only counted when another attempt succeeded — harder in a total failure, easier under partial success + bad model/max_tokens across hosts |
| EOF transport | 3 consecutive EOFs | Flaky shared path (proxy, LB) can accumulate under load |
| Stalled winner | Immediate, but only the crowned winner | One host at a time — not a true mass event |
Vote/gossip transport failures are intentionally ignored and do not quarantine.
Same deadlock after any of the above
Mass quarantine → admin unquarantine (or natural expiry) → all hosts enter no-winner probation (failure_strikes left after quarantine) → if no clean host remains, suspicious_winner_deferred forever.
Highest-likelihood mainnet analogues
- 404 / session missing after restart, bad register, or escrow already gone
- Timestamp drift during backlog / NTP issues
- 429/503 during PoC or overload if the race still fans out
- Transport if the gateway’s view of participant endpoints is wrong for everyone
Wrong-owner 403 is the cleanest demo on a small testnet; on mainnet, 404 + drift + overload are the realistic mass paths.
💬 Comments (3)
Q1: should we manage private key verification on the escrow side and prohibit the creation of escrow with an invalid private key?
Q2: should we reset quarantine count after the 30 mins period (now it decrease only 1, for example: 3 -> 2). Quarantine count can be changed through the env variable
Q3: should we reset the escrow quarantines when all participants already marked as quarantined?
@qdanik as for Q1: i think this may be my testnet trick - at the moment of creation all addresses were allwed to create escrows. this is not the case on Mainnet where we have limited number of allowed brokers, and they will configure the gateway correctly to sign with their private key i guess
🔄 Auto-synced from Issue #1506 every hour.
Gateway log evidence (testnet
.79)Full excerpt: https://gist.github.com/maria-mitina/1da0f9c348343d406b90483a2c8d5e4d
Image:
ghcr.io/gonka-ai/devshard-gateway:testnet-v4-localWhen: 2026-07-27 ~08:55–09:12 UTC
A) Wrong-owner escrow 23 → every host 403 → mass quarantine
After admin clear, chat against escrow 23 races all hosts; each returns
restricted to escrow ownerand is quarantined:B) Unquarantine again → still probation / no-winner on escrow 24
C) Hosts finish real content but nothing is crowned (client timeout)
Representative request
req-...-96— all 6 escrow hostssend_completedwith content, allsuspicious_winner_deferred,winner_nonce=0: