vike
Concepts

Startup preflight

A pure go/no-go gate that runs once before the first live order — clock skew, credential validity, disk headroom and network — with per-venue thresholds derived from measurement, a bound on every leg, and a demotion to paper that is enforced.

crates/vike-mount/src/preflight.rs opens with the sentence that justifies it: on mainnet day the first real order must not be the probe. A drifted host clock used to be discovered as a binance -1021 INVALID_TIMESTAMP on the first signed order, a dead key at the first submit, a full disk by the recorder and the journal dying in silence, a dead resolver by every reconnect failing at the hostname step. The preflight turns each into a structured report an operator reads at t=0.

Pure core, injected probes

Nothing in preflight.rs performs I/O. Every observation arrives through PreflightProbes, a four-method trait — local time, a venue's server time, a cheap authenticated read, free bytes under a directory — so the whole decision surface is unit-testable with no network, venue or filesystem. crates/vike-mount/src/startup.rs is the one production wiring: run_startup_preflight builds the real probes and vike_run::build_node runs it at the top of the venue mount. An unwired leg returns NO_PROBE, which never reads as a pass: the clock and disk legs warn — "could not measure" is not evidence of a fault — while the credential leg fails, because "we could not prove these keys work" must not mount a venue live.

The clock leg

A clock reading has four outcomes. A measured skew is judged against that venue's own thresholds; a venue that publishes a clock and did not answer warns; a venue declared to have no clock leg where a drift costs it nothing is not-applicable and never a warning; and a venue with no leg whose auth signs the clock into the order path warns as a disclosed gap — polymarket is the one such row. crate::server_time is the roster-gated table that declares which is which.

The thresholds are per venue because one global number was measurably false. The binance and bybit signers hard-code a 5000 ms recv_window, shared between local skew, one-way latency and venue queueing, so DEFAULT_CLOCK_FAIL_MS is 2500 — half the budget, past which one ordinary latency spike pushes orders outside the window — and DEFAULT_CLOCK_WARN_MS is 500. Only a venue that rejects an order over drift may fail: deribit and ig stamp no timestamp and hyperliquid binds the clock into a nonce valid for about a day, so they carry no fail threshold and warn at CANARY_CLOCK_WARN_MS, 1000 ms. The measurement that forced the split, from an NTP-disciplined prod2 on 2026-08-09: bybit read +9..23 ms, okx +10..37, aster +15..33 — while forty samples of hyperliquid's testnet node inside three minutes ranged from -220 to -424 ms. That is the venue's clock wandering, not the host's.

A reading is only as sharp as its round trip: the midpoint correction assumes a symmetric path, so the verdict is taken from what the reading proves — |skew| - rtt/2, floored at zero. Bybit's demo host once read 182 ms of apparent skew on a 549 ms round trip while every other sample read 13-23 ms; under the floor, a slow link cannot manufacture a warning. Up to DEFAULT_CLOCK_SAMPLES (3) readings are taken, the smallest round trip wins, and resampling happens only while the band still straddles a threshold — one call in every ordinary case.

Every leg is bounded, and the bounds are derived

Each clock read is a blocking REST call before the first venue mounts: seven wired venues, three samples each, on the shared 30 s agent is 630 s of possible stall. Two bounds replace that — a 3 s per-read CLOCK_READ_TIMEOUT, and a leg-wide budget from clock_budget_for of 1200 ms per venue actually read, clamped between 5000 and 15000 ms — so the worst case is 18 s at the ceiling and 11.4 s today. The fixed 5000 ms it replaced was sized against a six-read 1781 ms measurement; by 2026-08-22 a healthy pass summed to 2973 ms, so one dead venue needed 5973 and eight venues went unread. The credential leg is bounded separately: CREDENTIAL_PROBE_TIMEOUT is 5 s per attempt, CREDENTIAL_PROBE_ATTEMPTS is two — one retry, spent only on a probe that answered with an error, never on a timeout — and credential_budget_for is 3000 ms per probed venue clamped between 6000 and 20000.

The credential leg, and why the demotion is enforced

One probe is built for each of five venues — binance, bybit and okx through build_recon_client, plus alpaca and ctrader through their own bridge factories — whose credentials resolve and whose ceiling arms them, each performing that venue's own cheap authenticated read through its reconcile client. Every other reconciled venue builds its ReconClient inside its own mount arm, which this pre-mount step never calls, so it is never probed — and preflight never demotes a venue it did not check. ctrader authenticates during construction, so its probe defers construction into the closure, because building it eagerly would turn a refused grant into no row at all. Every probe's error names the host it could not reach.

For its whole life the report was advisory — a fail row was logged and the venue mounted live anyway, because a transient timeout raised the same fail as a refusal. CredentialGap removes that blocker. Rejected means the venue answered inside the bound and refused the signed read — a fail, and the venue is mounted paper for the session. Unanswered means nothing came back inside the bound — a warning, and the venue mounts as configured. The enforcement withholds every {VENUE}_-prefixed key from the map the mount reads, so make_engine sees absent credentials and lands on the paper fallback it always had: refused keys and absent keys are the same fact, and the old behaviour mounted live precisely when the venue had just said the keys do not work. One residual is stated: a fast non-auth error such as a 429 inside the bound reads as rejected and demotes; the retry stops a single blip doing it, and paper is the safe side.

A per-venue fail never flips the go bit. Only a global check — no disk headroom, no network — makes PreflightReport::go false, and even that does not ground the process: a daemon that refuses to start under Restart=on-failure crash-loops, so a global fail is logged at error and the mount proceeds.

Disk, network, and what the arming ceiling gates

The disk leg was dark for a year because free space is not reachable from std; it is armed on unix through a safe rustix::fs::statvfs and reports a declared reason on Windows, warning below 5 GiB and failing below 1 GiB over directories the caller passes — which is how it measures the journal directory the mount actually built. The network leg spawns a NetProbe and waits at most 500 ms for its first round.

Every venue set is derived through would_mount_live_under_policy — the same predicate the mount arms on, so preflight and mount cannot disagree about what is armed. Until that was threaded, a deployment whose policy said ig = "paper" still had IG contacted at every start, with a credentialed read. policy: None reads all-paper and contacts nothing; a box with no credentials, or a full store under an all-paper policy, makes not one network call. VIKE_PREFLIGHT_SKIP=1 — the exact string, read from the real process environment — removes every leg. Probe errors never carry a key, secret or passphrase, the report's lines read [FAIL] credentials (okx): … — remediation, and the arming ceiling stays the gate consulted above all of this.

On this page