The dead-man's switches
Two of them, with opposite defaults because they read opposite evidence — the connection switch is on unless you turn it off and trips on a link the venue's own bridge reports dead; the silence switch is off unless you write the key and trips on a feed that has gone quiet, which is also what a closed market looks like.
A dead-man's switch is the automatic counterpart to the manual halt sentinel: when nobody is in front of the daemon, something cancels the resting orders anyway. Vike Trader ships two of them, and one sentence separates them — what each one accepts as evidence that the daemon has lost the market:
- The connection dead-man reads the link state a venue's own bridge discloses. A feed reported
Disconnectedfor longer thanpolicy.toml'slink_deadman_grace_mscancels that venue's resting orders. The key absent means ARMED, at two minutes. - The silence dead-man reads ingest. No venue event, tick, bar, quote, trade or book update on
any venue for longer than
deadman_timeout_mscancels every resting order. The key absent means OFF, with one warning at mount.
The defaults point in opposite directions because a market that closes is silence and is not a disconnect. The connection switch cannot see a close, so it may be armed by default; the silence switch cannot tell one from an outage, so it may not.
| connection dead-man | silence dead-man | |
|---|---|---|
key in policy.toml | link_deadman_grace_ms | deadman_timeout_ms |
| absent | armed at DEFAULT_LINK_DEADMAN_GRACE_MS — 120 000 ms | off, warned about once |
| off is spelled | = 0, the only off | = 0, or left out |
| evidence | FeedStatus::Disconnected per (venue, symbol) | nothing arrived, anywhere |
| a session close | invisible — a close reads Stale, which never counts | trips it |
| a trip cancels | the venue whose link died | every venue |
| where it arms | a per-venue table and a disclosing lane in this mount | wherever the key is written |
| authority | crates/vike-core/src/runtime/link_deadman.rs | crates/vike-core/src/runtime/deadman.rs |
They are independent: either, both or neither may be on. They are separate CoreConfig fields, and
they share exactly one setting — deadman_action, because what a trip does is the same question
for both and a second key would be a second answer to it. Both are folded from policy.toml by
crates/vike-tradehub/src/tradehub_cli.rs's live_mount_with and by nothing else: the daemon's own
paper rehearsal arm constructs neither, and neither does vike-desktop or
crates/vike-run/src/bin/ibkr_mount.rs.
The connection dead-man
The signal is the per-(venue, symbol) FeedStatus the bridges disclose. It reaches the core through
two seams that the latch cannot tell apart: a feed that owns a LiveDataSink maps the data layer's
StreamStatus 1:1 by crates/vike-core/src/core_sink.rs's stream_status, and a feed that owns no
sink — the HFT tick pumps, which take a bare vike_exec::TickSender — pushes a FeedStatus onto
the tick lane directly, through the one map in
crates/vike-bridge-core/src/stream_health.rs's HealthEvent::feed_status. Both land on the same
Ingest::StreamStatus arm. Disconnected opens a grace window for that key; Live for the same
key closes it and re-arms the link; and Stale never counts, in either direction. That last line is the entire difference between this
switch and the one below. Stale means "no fresh price exists", which is an accurate disclosure of
a weekend on an FX venue and of a scheduled venue maintenance window — reacting to it would be the
silence switch wearing a new name.
A Disconnected opens the window whether or not that link was ever seen Live, and that is a
correction rather than a simplification: no bridge on this roster discloses a Live on a first
successful connect — vike_bridge_core::stream_health's StreamHealth::recover returns nothing
when no gap is open, so Live exists only to close one. A gate on a prior Live would have made
the switch blind to the first socket death of a mount, which is the outage it exists for. The
startup dial-up is absorbed by the grace instead.
Recovery is disclosed on the first frame back, never at connect, and that difference is what
keeps a flapping venue disclosed down. A successful dial proves only that the venue accepted a
TCP+TLS handshake; an inbound frame — data, or a server ping the socket answers — proves the wire
carries traffic again. On a connect-time Live, a venue that accepts a connection and drops it in a
loop (a rate-limit ban, a half-dead edge) would emit Live/Gap on every cycle and reset the grace
window each time, so the switch could never trip on the outage it is most needed for. One
Disconnected per outage, for the mirror-image reason: each feed owns one StreamHealth
(crates/vike-bridge-core/src/stream_health.rs) across its whole lifetime, re-dials included, so a
venue that flaps for ten minutes discloses one disconnect and one recovery rather than three hundred.
crates/vike-core/src/runtime/link_deadman.rs is a clock-free latch — two fields per link, both
entry points taking an injected now_ms — evaluated at the drain-loop boundary on the
timer wheel, on its own LinkDeadManSweep cadence of half the grace. An
entry exists only for a link that has actually been disclosed down at least once, and a status
change on an unarmed venue costs one set lookup and no allocation. The per-message fold has no
branch for the feature at all.
The bounds are refused by name, never clamped
crates/vike-config/src/policy.rs accepts absent, 0, or MIN_LINK_DEADMAN_GRACE_MS (30 000 ms)
through MAX_LINK_DEADMAN_GRACE_MS (3 600 000 ms). The floor is the measured worst ordinary
reconnect across the lanes that report a dead link, which is the L2 depth driver: a bounded 10 s
dial, a 10 s REST book seed and a 3 s backoff, 23 s end to end. It is the slowest of the three — the
tick pumps a live vike-tradehub mount actually subscribes come in at 21 s and under — so anything
under the floor makes the switch
trip on the re-dial that follows an ordinary socket fault, which is the false-trip class the design
exists to remove. Above the ceiling it is a switch that never fires. A value outside the range is
rejected at load with the key and the file named, because a grace that was silently raised is a
grace the operator believes they set and do not have.
Which venues actually arm
Arming needs two independent facts to be true, and the second one is a fact about your daemon rather than about the venue.
One: the venue's row. crates/vike-model/src/link_deadman.rs carries one named row per roster
venue, each read from the emitter it cites, with three dispositions. Armed — the venue trades
continuously and its bridge discloses a disconnect: binance, aster, bybit, okx — each from
two emitters, both on a book socket: the DOM depth feed and the HFT quote/trade/book tick pump —
and polymarket (from its primary book feed). SessionBounded — the
bridge does disclose one, but the market has sessions and a close has not been observed to read as
Stale rather than as a gap: oanda and ig, off until somebody watches a weekend through.
Inert — the bridge hands a gap to the sink nowhere at all, so the switch could not fire even if it
were armed: hyperliquid, deribit, alpaca, ibkr, ctrader, fxcm, dukascopy. Each unarmed row
carries its reason verbatim, and the daemon prints it.
Two: whether this mount subscribes a lane that carries the disclosure. A row is a statement
about the adapter, never a promise about a mount. crates/vike-tradehub/src/tradehub_cli.rs's
mount_link_disclosure is the reading, one arm per venue plan, and for a CEX venue the answer is
Discloses. That it is an answer at all is recent, and the history is the point: the daemon's
VenuePlan::Cex arm subscribes the kline lane and the quote/trade/book tick pump and deliberately
never subscribe_depth (crates/vike-tradehub/src/feeds.rs), and until 2026-09-06 neither of those
two lanes disclosed anything — the kline lane rides vike_bridge_core::market_pump, which carries
no link state at all, and the tick pump owns no LiveDataSink. The pump now discloses its own
transport state onto the core tick lane it already writes ticks to: disclose_link in
crates/bridges/binance/src/family/depth.rs's md_main, and its twins in
crates/bridges/bybit/src/market_data.rs and crates/bridges/okx/src/market_data.rs.
Subscribing the DOM feed instead was the other way to close it, and it was rejected: that pump
already carries the venue's @depth-class book stream on the same host, so subscribe_depth would
open a second socket to the same stream, keep a second copy of the same book and add a periodic REST
re-seed — for a signal the first socket already has, delivered into l2_snapshot, which is a no-op
in every sink this daemon owns. The disclosure costs zero extra bytes on the wire this way, and the
core's per-message fold is unchanged: nothing new rides the tick, quote, trade, bar or book path,
only the occasional control event that was already there.
A default vike-tradehub daemon now arms four venues
Fold the two together. binance, bybit, okx and aster arm on a default live mount: the tick pump
is the lane that mount subscribes, and it is the venue's book socket rather than a side channel.
Polymarket is a fifth, behind its Cargo feature — its subscribe_book on every seated token
feeds a MakerSink that forwards stream_status. Every session-bounded venue — the FX and equities
mounts — stays off deliberately, because a market closing is not an outage: until a weekend has been
watched through and seen to disclose Stale rather than a gap, the conservative row holds. Every
Inert venue stays off because nothing in its bridge would reach the latch.
No row flipped direction. This was a change of reach, not of classification: those four were
always Armed in the venue table, and the mount simply could not hear them — so
link_deadman_config_from_policy returned None and built no latch at all on the binary that signs
real orders, whatever policy.toml said. A daemon mounting only session-bounded or Inert venues
is still that case, and it is the ordinary state of an FX-only daemon: no latch, no timer, no
cadence contributed, and no automatic stop for a dead link. Nothing in policy.toml changes which
venues arm — that is a property of the bridge and of the lanes this mount subscribes.
The mount says which it is rather than leaving it to be discovered: link_deadman_arming_report
emits one
line per mounted venue at startup, and it keeps apart the four ways a venue ends up unarmed — its
market has sessions, its bridge discloses nothing, this daemon subscribes no lane that carries it,
or the operator wrote 0. Read those lines; they describe your daemon and this page does not. The
mount-lane case is checked before the armed line is printed, which is the whole of the fix: an
earlier version logged LINK DEAD-MAN ARMED for binance on a daemon where nothing could ever reach
the latch.
What an operator does about it
There is no line that arms the connection switch, because leaving the key out arms it. The lines that exist are the ones that change it:
# <project>/settings/policy.toml
link_deadman_grace_ms = 45000 # a tighter grace; legal range 30000..=3600000
link_deadman_grace_ms = 0 # OFF, for every venue — the only off
deadman_action = "cancel_all" # a trip pulls the book and leaves the trading state aloneNeither key can be reached from the environment or the command line:
Policy implements neither EnvOverride nor CliOverride and
both traits are sealed, so an override is a compile error rather than a review comment. A stale
service-unit line cannot disarm a kill switch with no diff to review.
What this file cannot do is arm a venue. The row is a property of the bridge and of the mount, not a preference: a venue whose feed hands the core no disconnect has nothing to observe, and a venue whose only emitter is a lane this daemon does not subscribe has nothing to observe here. Both are code changes, and the startup lines name which of the two you are looking at.
The silence dead-man, and the morning it was default-on
The older switch is crates/vike-core/src/runtime/deadman.rs: a four-field latch, deliberately
clock-free, whose observe records a monotonic maximum over every liveness message — venue events
and market data, never control or waker messages — so an out-of-order older frame cannot drag
freshness backward. check returns the action exactly once when the newest observed timestamp has
aged strictly past the threshold (now − newest > timeout; a lag equal to the timeout is still
fresh) and the switch has not already tripped this outage; fresh data clears the latch and re-arms
it. A core that has seen no data never trips, so pre-feed startup is silent rather than a nuisance.
It shipped armed for one morning, and a closed market is what reversed it
The key shipped with a compiled-in 60 s default — armed unless an operator wrote 0 — and the
ruling was reversed the same day, once review established what the switch actually observes. It
counts ingest and deliberately nothing else: not a control command, not the periodic waker, and not
the one message that says a socket is alive. A transport heartbeat never reaches the core at all. So
it cannot tell a dead socket from a quiet market or a closed one, and an armed default bought three
halts nobody asked for. The session-close halt, deterministic: on FX over the weekend and on
equities every evening it trips about a minute after the last tick, at every close — cancelling a
GTC deliberately left over the weekend, halting every engine, writing the sentinel, and opening the
next session halted until an operator deletes the file. The thin-market halt, probabilistic: a
live mount following one illiquid instrument goes sixty seconds without a tick in a quiet minute.
And the cap that could not be raised past the close: the maximum timeout is one day and an FX
weekend is about 48 hours, so the only legal settings on an FX venue were 0 or a switch that trips
every Friday. crates/vike-config/src/policy.rs's Policy::deadman_timeout_ms carries the record.
The safety argument was right; the mechanism was the wrong one for it, and the connection switch
above is the one it wanted.
So this key is now the optional extra for a 24/7 mount — a crypto perp venue that never closes
and always ticks — that also wants a silence detector. Absent is off; and because the daemon cannot
tell "the operator decided against it" from "the operator never heard of it", an absent key warns
once at mount. deadman_timeout_ms = 0 is the only spelling that records the decision and silences
that warning:
# <project>/settings/policy.toml
deadman_timeout_ms = 60000 # arm it — the recommendation for a venue that never closes
deadman_timeout_ms = 0 # a deliberate no, and the only thing that silences the warningNothing arms it silently: 60 000 ms is RECOMMENDED_DEADMAN_TIMEOUT_MS, the number the warning
suggests, not one any binary applies. Accepted values are absent, 0, or 1 000 ms through one day,
rejected by name rather than clamped for the same reason as the sibling's bounds. Raise it for a
thin symbol; leave it off on anything that closes.
What a trip does
Both switches act at the drain-loop boundary on the timer wheel, never on
the per-message fold, each on its own cadence of half its window — so a deadline is evaluated at
least twice before it expires. crates/vike-core/src/runtime/watchdog.rs holds both acting halves,
sweep_deadman and sweep_link_deadman. On trip each one:
- emits one
tracing::warn!per outage or dead link — a fault transition, within the logging budget; - cancels through the one order-write vocabulary,
apply_intentwithOrderIntent::MassCancel, so nothing bypasses mint, route or gate — and that verb also clears every armed conditional book, because a dead-man pulls all exposure. The cancel is taggedCancelIntent::RiskOff: a cancel issued because the market was lost must not be shed by a venue's own cancel budget; - under
DeadManAction::CancelAllAndHalt— the defaultdeadman_action— halts every engine in-process and then writes the sentinel best-effort, a failed write logged rather than panicking the core, since the in-process gate has already stopped new orders; - pushes a line into the recent-events ring and marks the snapshot dirty.
The scope is where they differ. The silence switch cancels with venue: None, symbol: None —
everything. The connection switch scopes the cancel to the venue whose link died, across every
engine of that exchange, so a two-account binance mount has both books pulled and a bybit socket
death leaves the binance book resting. One cancel pass per venue rather than per link: two symbols
of one venue expiring in the same sweep are one exchange fact.
HALT is the exception to that scoping and is deliberately process-wide, because the sentinel is one
file by construction and a half-halted daemon is a state nobody asked for. It is the same file a
manual touch HALT writes and every venue's submit boundary checks — the path is supplied by the
binary through vike_bridge_core::halt::halt_path_from_env, because environment reads stay in
binaries and vike-core takes no bridge dependency — so the automatic trip and an operator's hand
reach one file. A recovered link re-arms its own latch for the next outage; Halted and the
sentinel do not clear, which is an operator's decision.
It cancels; it does not flatten
A trip pulls resting orders and, at most, halts. It never sends a closing order, so a position open
when the link died is still open after the trip. Getting out is the operator's market-exit, usable
from a halted core precisely because a flatten leg is the position-covered reduce the halt admits —
see halt. And because the halt is engaged, the strategy cannot re-open risk
when data returns until the operator clears it.
What it costs replay
The boundary waker message is journaled only when a wall-clock sweep that mutates order state can
ride it, and there are now three such sweeps: the stuck-order watchdog
and both dead-man switches. That waker record is what makes replay_from return Unsupported for
the session: a trip is a wall-clock effect the journal cannot re-derive. A core with none of the
three on writes no waker record and replays normally; the rule is on the
replay page.
In practice a live mount has already forfeited replay before either key is read — live_mount_with
sets a 30 s submit-acknowledgement timeout, which arms the same record — so arming either switch
forfeits nothing further. It is the paper and harness cores, which set no such timeout, where the
distinction is live.
Both switches are among the mechanisms that reach Halted on their own — the fold panic's
safe-state entry is another — which is why the halt policy has no mode that refuses a reducing
order: a switch that trips and then traps the operator in the position it just exposed would be
worse than no switch.
Emulated orders
The core keeps its own book of stop and trailing-stop arms for venues and strategies without native conditional orders; arming bypasses the risk gate, and only the fire crosses it, as a plain market order.
The watchdog
The core's cold-path sweeps — a confirm-grace ladder for orders stuck before acknowledgement, a reject-free early confirm, managed expiry, safe-state entry after a panic, the margin-call sweep and the drawdown latch.