1 · One node, and what it can't do
Rift is a Mountebank-compatible mock server written in Rust. You POST an imposter: a fake service bound to a TCP port, carrying an ordered list of stubs. Each stub is a predicate (“GET /orders/*”) and a response. Your system under test calls the imposter as if it were the real service, and afterwards your test asks Rift what it received.
Some stubs are stateful. A scenario stub only matches when a little state machine is in a given state (requiredScenarioState: "AwaitingPayment") and may move it on (willSetStateTo: "Shipped"). That state belongs to a flow: one test run, one user session, named by a flow_id taken from a header such as X-Flow-Id. Stubs can also cycle through several responses, or proxy to a real upstream once and replay the recording forever (proxyOnce).
A single Rift process serves 20–40k requests per second, so one node usually has enough compute. What one node can't give you is an environment that survives that node dying. The cluster meets four requirements, and each one maps to a specific mechanism:
| # | Requirement | How it is met |
| R1 | A config change acknowledged by any node is servable from every node by the time the client sees its 2xx. | Config is a Raft log every node applies; the write barrier holds the 2xx until every member has applied it (section 6). |
| R2 | A flow-state change made while serving one request is visible to the very next request, whichever node gets it. | Each flow has one owner, and state that decides which stub matches is read from it (sections 9 and 10). |
| R3 | Nothing is lost on a full-cluster restart: configs and flow state. | The Raft log is fsynced on a majority before commit; flow state is written to disk on three nodes (sections 12 and 13). |
| R4 | An accepted admin request is never lost, even if the node handling it dies mid-flight. | The receiving node parks the request on its own disk before forwarding it, and replays it until it commits (section 6). |
One constraint shapes everything: no mandatory external dependency. There is no etcd, Redis or ZooKeeper; the Rift binaries behind the load balancer are the cluster.
2 · The fleet and the load balancer
The cluster is replicated, not sharded. Every node holds the full imposter set, binds every imposter port, and can answer any mock request. That is what makes the load balancer trivial: it can send anything anywhere, and losing a node loses no configuration.
Round-robin traffic onto an active-active fleetanimated · every node binds :8080 and :9090
Blue: mock requests. Dashed grey: the private cluster port (Raft and owner RPCs, every message HMAC-signed).
Traffic reaches an imposter in one of three ways: on its own port (the Mountebank default), on one shared port with stubs separated by a space that matches the flow id, or through the gateway, where the port is carried in the path: /__rift/8080/orders is dispatched in-process to imposter 8080 as /orders. On Kubernetes, where Service ports are fixed, the gateway and the front-door route table are how runtime-minted ports stay reachable.
The load balancer needs no stickiness, and stickiness would not provide correctness (D-13). A balancer that hashes the flow-id header hashes onto its own ring over its own endpoint list. It never computes the cluster's ownership function, so the node it picks is the flow's owner about 1/N of the time, by chance. Correctness comes from the cluster routing each stateful operation to the owner, wherever the request landed.
Placement is a latency optimization. Ownership is a correctness mechanism. They are computed by different systems and must be assumed to disagree.
3 · Two planes
State is divided by what would go wrong if two nodes disagreed about it, and each class gets the cheapest mechanism that makes disagreement impossible. Configuration and membership are rare, small and must be identical everywhere, so they go through consensus. Flow state changes on nearly every stateful request, so it goes to a single owner per key instead.
Control plane
Raft · linearizable · durable · needs a quorum
- Membership: voters and learners
- Imposter configs and their
enabled bit - Front-door route table and its revision
- Admin intent log and op-id dedup
Changes at human or CI speed. Can afford a quorum round-trip and an fsync per write.
Data plane
per request · no consensus, ever
- Stateless stub matching: 100% in-process
- Flow state (scenario FSM, flow KV): one owner per key, chosen by hashing, plus 2 async replicas and a write-ahead tier
- Request journal: upstream's own, per node
- proxyOnce claims: a small state machine at the owner
Changes up to tens of thousands of times a second. A stub with no stateful features never touches the cluster.
committed membership (control plane) ──feeds──▶ the ownership ring (data plane)
The arrow between them is what ties the planes together. Membership lives in the Raft log, so at any log index every node computes byte-identical membership, and therefore byte-identical owners for every flow. Two nodes can never disagree about who owns a flow at the same index. The rule of thumb:
Raft carries what you'd expect to survive a restart. The ownership ring carries what a running test mutates.