<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mohsen Zainalpour</title>
    <description>The latest articles on DEV Community by Mohsen Zainalpour (@mohsen_zainalpour_a20355e).</description>
    <link>https://dev.to/mohsen_zainalpour_a20355e</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2000415%2F500fd86e-dfc0-49c8-896f-9b38e7f5019c.jpg</url>
      <title>DEV Community: Mohsen Zainalpour</title>
      <link>https://dev.to/mohsen_zainalpour_a20355e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohsen_zainalpour_a20355e"/>
    <language>en</language>
    <item>
      <title>Packet classification, for mock servers</title>
      <dc:creator>Mohsen Zainalpour</dc:creator>
      <pubDate>Tue, 11 Aug 2026 11:39:07 +0000</pubDate>
      <link>https://dev.to/mohsen_zainalpour_a20355e/packet-classification-for-mock-servers-32np</link>
      <guid>https://dev.to/mohsen_zainalpour_a20355e/packet-classification-for-mock-servers-32np</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkdyzxknvj4zv4w7mgg3m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkdyzxknvj4zv4w7mgg3m.png" alt=" " width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every HTTP mock server does the same three things. It accepts a request, decides which of your configured stubs that request matches, and writes the corresponding response.&lt;/p&gt;

&lt;p&gt;The first and third are solved problems. The second is where the engineering is, and it is the one nobody thinks about, because at the size where you first meet it, it is free.&lt;/p&gt;

&lt;p&gt;Here is the part that is not free: its cost is a function of your config file, and your config file only ever grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  The shape of the problem
&lt;/h3&gt;

&lt;p&gt;The obvious implementation is the one you would write on a whiteboard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;stub&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stubs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predicates&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;default_response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is correct, it is obviously correct, and it is what you should write first. Its cost is O(stubs) per request, with a constant set by how expensive your predicates are. At twelve stubs of &lt;code&gt;equals&lt;/code&gt; predicates it is a rounding error against the cost of reading the socket.&lt;/p&gt;

&lt;p&gt;Now run it for four years. Nobody plans to have three hundred stubs. You have twelve. An integration lands and brings eight. Someone mocks the error path, then the timeout path, then the malformed response. A contractor adds forty for a migration that shipped in 2023 and nobody deleted them, because deleting a stub is how you discover which test depended on it.&lt;/p&gt;

&lt;p&gt;And the predicates get more expensive as they get more specific. An &lt;code&gt;equals&lt;/code&gt; on a path is a string comparison. A &lt;code&gt;matches&lt;/code&gt; is a regex. A &lt;code&gt;jsonpath&lt;/code&gt; or &lt;code&gt;xpath&lt;/code&gt; predicate parses the body — and if a body-inspecting predicate is in the scan, you may be parsing the same request body once per candidate stub, per request.&lt;/p&gt;

&lt;p&gt;I measured the shape against the two engines that scan. Both tables below hold engine, host and load fixed and vary only how much matching work a request implies — but they do it in slightly different ways, and that is worth naming rather than smoothing over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mountebank 2.9.1&lt;/strong&gt;, Apple M4 laptop, &lt;code&gt;oha&lt;/code&gt; at 50 keep-alive connections, 20s per scenario after warmup, median of 3 runs, measured 2026-07-20. This is the strict version of the question: one 310-stub imposter throughout, moving only from the stub that matches first to the stub that matches last. Corpus and predicate shape are held constant, so &lt;em&gt;position in the scan&lt;/em&gt; is the only variable.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;first match&lt;/th&gt;
&lt;th&gt;last match&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mountebank&lt;/td&gt;
&lt;td&gt;8,546 RPS&lt;/td&gt;
&lt;td&gt;1,344 RPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rift&lt;/td&gt;
&lt;td&gt;211,378 RPS&lt;/td&gt;
&lt;td&gt;209,523 RPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;WireMock 3.9.1&lt;/strong&gt;, Xeon Platinum 8573C at 16 vCPU, &lt;code&gt;oha&lt;/code&gt; at 256 keep-alive connections, measured 2026-07-27, with WireMock's Jetty pool pinned to 256 so its 10-thread default was not doing the losing for it. This is the looser version: a one-stub imposter against a 310-stub deep path match, so corpus size and path depth move together and the fall is both effects at once.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;1 stub&lt;/th&gt;
&lt;th&gt;310-stub deep match&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WireMock&lt;/td&gt;
&lt;td&gt;83,048 RPS&lt;/td&gt;
&lt;td&gt;24,264 RPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rift&lt;/td&gt;
&lt;td&gt;334,025 RPS&lt;/td&gt;
&lt;td&gt;326,779 RPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;p99 latency on that second column: 31.6 ms for WireMock, 2.5 ms for Rift.&lt;/p&gt;

&lt;p&gt;Do not read across the two tables. Different hosts, different concurrency levels, different days — Rift itself reads 211k on one and 334k on the other, which tells you how much of any cross-table comparison would be hardware rather than engine. Each table is sound read downwards, and nothing else.&lt;/p&gt;

&lt;p&gt;Two engines lose 71% and 84% of their throughput between the first stub and the last. One loses under 1% and 2%. The multiple is the least interesting thing on either table. &lt;strong&gt;The slope is the argument.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  It is packet classification, and it has been solved since the 1990s
&lt;/h3&gt;

&lt;p&gt;Here is the reframe that made this tractable, and I want to be exact about the fact that I did not invent any of it.&lt;/p&gt;

&lt;p&gt;"Which of these N rules matches this packet?" is the central question of network firewalls and routers. A packet arrives; some hundreds or thousands of ACL rules each specify constraints over source address, destination address, protocol, port ranges; exactly one — the first match, by rule order — decides what happens to it. This is called &lt;strong&gt;packet classification&lt;/strong&gt;, it has a literature going back to the mid 1990s, and nobody in that field has evaluated rules in a loop for a very long time.&lt;/p&gt;

&lt;p&gt;Now read the mock-server problem again. A request arrives; some hundreds of stubs each specify constraints over method, path, headers, query, body; exactly one — the first match, by declaration order — decides the response.&lt;/p&gt;

&lt;p&gt;It is the same problem. Different attribute names, identical shape: multi-dimensional first-match-wins classification over a rule set that is large, mostly static, and known in advance.&lt;/p&gt;

&lt;p&gt;The technique Rift uses is the &lt;strong&gt;Lucent bit-vector algorithm&lt;/strong&gt; (Lakshman and Stiliadis, 1998). The idea is almost embarrassingly simple once stated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give every rule an id equal to its position in the ordered rule list.&lt;/li&gt;
&lt;li&gt;For each dimension independently, precompute which rules that dimension can &lt;em&gt;rule out&lt;/em&gt; for any given input.&lt;/li&gt;
&lt;li&gt;At query time, ask each dimension for a bitset over rule ids — the rules it cannot eliminate.&lt;/li&gt;
&lt;li&gt;Intersect the bitsets. Walk the surviving bits in ascending order.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the whole thing. Every dimension prunes on its own, the intersection is the candidate set, and the first surviving bit is the answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  The freebie that made me trust it
&lt;/h3&gt;

&lt;p&gt;Step 1 above is doing more work than it looks like it is doing.&lt;/p&gt;

&lt;p&gt;A stub's id is its position in the declaration-ordered stub vector. A candidate set is a dense bitset over those ids. Iterating a bitset yields set bits in ascending numeric order.&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;ascending bit order is Mountebank's first-match-wins order.&lt;/strong&gt; Not "is equivalent to." Is. The ordering semantics are not implemented anywhere; they fall out of the representation. There is no sort, no priority comparison, no code path where a refactor could get precedence subtly wrong, because precedence is not a decision the matcher makes.&lt;/p&gt;

&lt;p&gt;That is the property that convinced me this was the right structure rather than a clever one. In a compatibility-constrained engine, the semantics you cannot accidentally break are worth more than the ones you have tests for.&lt;/p&gt;

&lt;p&gt;One honest limit on that guarantee: it fixes the &lt;em&gt;order&lt;/em&gt; of the candidates, not their &lt;em&gt;membership&lt;/em&gt;. Which brings up the obvious question.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you know you did not drop a match?
&lt;/h3&gt;

&lt;p&gt;You constrain the index in one direction only.&lt;/p&gt;

&lt;p&gt;Every dimension's bitset is &lt;code&gt;matched_bits | always_bits&lt;/code&gt; — the stubs whose constraint the request satisfies, &lt;strong&gt;plus&lt;/strong&gt; every stub that either does not constrain this attribute at all or constrains it in a shape this dimension cannot index. The invariant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A dimension may only ever exclude a stub it can &lt;em&gt;prove&lt;/em&gt; cannot match.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the candidate set is a strict over-approximation: a superset of the true matches. Full predicate evaluation still runs on every survivor, using the unchanged Mountebank semantics, which remain the only source of truth about whether a stub matches. The index never decides that a stub &lt;em&gt;does&lt;/em&gt; match. It only ever decides which stubs are not worth asking about.&lt;/p&gt;

&lt;p&gt;That asymmetry is what makes the design safe to extend. A dimension that keeps too many stubs costs performance and nothing else, so widening one later is a pure optimisation and never a semantics question. A dimension that wrongly excludes a stub makes it silently stop matching — which is the worst failure this system can have, and the reason a differential test (&lt;code&gt;differential_index_matches_linear_oracle&lt;/code&gt;) runs the index against a linear oracle and fails on any under-approximation rather than trusting the argument I just made.&lt;/p&gt;

&lt;p&gt;This is a rich enough topic that it deserves its own piece, and it will get one. For now the thing to take is the shape: a prefilter that is allowed to be wrong in exactly one direction is a different, much more tractable engineering problem than a matcher that has to be right.&lt;/p&gt;

&lt;h3&gt;
  
  
  Six dimensions, and one of them in detail
&lt;/h3&gt;

&lt;p&gt;Rift's index is six dimensions over four request attributes — the path carries three of them, one each for exact, literal and regex constraints.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Indexes&lt;/th&gt;
&lt;th&gt;Structure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Method&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;equals&lt;/code&gt; on method&lt;/td&gt;
&lt;td&gt;eight fixed slots — seven common verbs plus an "other" bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Path, exact&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;equals&lt;/code&gt; on path&lt;/td&gt;
&lt;td&gt;hash map on the case-folded path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Path literals&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;startsWith&lt;/code&gt; / &lt;code&gt;contains&lt;/code&gt; / &lt;code&gt;endsWith&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Aho-Corasick automaton&lt;/strong&gt; over every anchor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Path regexes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;matches&lt;/code&gt; on path&lt;/td&gt;
&lt;td&gt;multi-pattern automaton, all matching pattern ids in one search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Body, whole&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deepEquals&lt;/code&gt; on a JSON body&lt;/td&gt;
&lt;td&gt;structural hash of the expected body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Body, field&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;equals&lt;/code&gt; on body fields&lt;/td&gt;
&lt;td&gt;a &lt;a href="https://crates.io/crates/quamina" rel="noopener noreferrer"&gt;quamina&lt;/a&gt; field automaton&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rather than tour all six, take the third one, because it is the one where the cross-domain borrowing pays most visibly.&lt;/p&gt;

&lt;p&gt;Path literal predicates are the awkward middle of the space. &lt;code&gt;equals&lt;/code&gt; hashes. &lt;code&gt;matches&lt;/code&gt; needs a regex engine. But &lt;code&gt;startsWith("/api/v2")&lt;/code&gt;, &lt;code&gt;contains("/internal/")&lt;/code&gt; and &lt;code&gt;endsWith(".json")&lt;/code&gt; are neither — they are substring tests, and the naive index for them is a bucket per anchor string that you walk one at a time. With 200 literal anchors, answering "which of these appear in this path" means 200 substring searches over the same short string.&lt;/p&gt;

&lt;p&gt;Aho-Corasick answers all of them in one pass. You build a trie of every anchor, add failure links so that a mismatch resumes at the longest proper suffix that is still a viable prefix, and then run the path through the resulting automaton exactly once. Every anchor that occurs anywhere in the path reports itself, in time proportional to the length of the path plus the number of matches — not to the number of anchors. Anchoring for &lt;code&gt;startsWith&lt;/code&gt; and &lt;code&gt;endsWith&lt;/code&gt; is a position check on the reported match, which is free once you have the match.&lt;/p&gt;

&lt;p&gt;This is a 1975 algorithm. It is what &lt;code&gt;fgrep&lt;/code&gt; was built on. The reason it belongs here is not that it is fast in the abstract but that it changes what the cost is a function of: from "how many literal predicates have you written" to "how long is the path you are matching." Those are very different curves, and only one of them is under your users' control.&lt;/p&gt;

&lt;p&gt;Dimension 4 is the same move against regexes. Rift's own slow path used to be regex predicates, because you cannot hash-dispatch a regex: at the 100th pattern it managed roughly 54k RPS. Replacing the per-pattern loop with a single multi-pattern automaton — one overlapping search that reports every matching pattern id — took that to about 207k, in line with every other predicate type. That is not a micro-optimisation. It is a change of complexity class, from one search per pattern to one search.&lt;/p&gt;

&lt;h3&gt;
  
  
  The order the dimensions run in
&lt;/h3&gt;

&lt;p&gt;They fold cheapest-first, and the fold short-circuits the moment the candidate set is empty. A request no stub can match usually stops after the first dimension or two, which is the case that matters most in practice — misses are common and they should be cheap.&lt;/p&gt;

&lt;p&gt;The two body dimensions run last, because they are the only ones that touch the request body. By the time they run, the method and path dimensions have already emptied the accumulator in most cases, so the expensive parse is skipped rather than optimised. When it does run, the body is parsed as JSON &lt;strong&gt;once per request&lt;/strong&gt; and shared across both dimensions, rather than each predicate reaching for the raw bytes independently.&lt;/p&gt;

&lt;p&gt;A dimension that indexes nothing is skipped entirely rather than paying a full-width copy and intersect to learn nothing. This matters more than it sounds: it means an imposter the index cannot help degrades to a plain scan instead of paying for an index it is not using.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the bitsets are hand-rolled
&lt;/h3&gt;

&lt;p&gt;The obvious question from anyone who has reached for &lt;code&gt;roaring&lt;/code&gt; or &lt;code&gt;fixedbitset&lt;/code&gt;: why write your own?&lt;/p&gt;

&lt;p&gt;Because the operation set is tiny. Intersect, union, iterate ascending. That is all a candidate set ever needs to do, and compressed bitmaps buy their compression back in branchy decode paths that a dense word vector does not have.&lt;/p&gt;

&lt;p&gt;The sizes involved make the decision easy. 4,096 stubs is 512 bytes — one bitset fits in L1 several times over, word-wise &lt;code&gt;AND&lt;/code&gt; autovectorises, and the whole intersection is a handful of cache resident SIMD instructions. Roaring's advantage arrives when your universe is sparse and enormous. A stub corpus is neither.&lt;/p&gt;

&lt;p&gt;Dimensions are concrete struct fields rather than &lt;code&gt;Box&amp;lt;dyn Dimension&amp;gt;&lt;/code&gt;, so the fold dispatches statically. Matching is not allocation-free, and I would rather say so than let you find out: the accumulator and a per-dimension scratch bitset are allocated per request, and a path containing uppercase bytes allocates a folded copy.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this does not buy you
&lt;/h3&gt;

&lt;p&gt;The section above is the optimistic half. Here is the other one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The index helps only when stubs are distinguishable on an indexed attribute.&lt;/strong&gt; An imposter where every stub is a body regex indexes on nothing, falls back to the scan, and is exactly as linear as Mountebank — with a better constant, and that is all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full evaluation always runs.&lt;/strong&gt; There is nothing for a prefilter to save on a one- or two-stub imposter, which is why the simple-stub scenario is among the &lt;em&gt;lowest&lt;/em&gt; multiples in every table I publish — 4.0x against WireMock, 24x against Mountebank. The large multiples come from scenarios where the index removes work, not from the implementation stack alone. Where something other than matching dominates the request, such as response templating, the multiple is lower still.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Indexing is not the same as being fast, and one of the comparisons proves it.&lt;/strong&gt; &lt;a href="https://microcks.io/" rel="noopener noreferrer"&gt;Microcks&lt;/a&gt; is also indexed rather than scanning, and it is also flat by stub position: &lt;strong&gt;6,457 → 6,447 → 6,420 RPS&lt;/strong&gt; across the first, middle and last of the same 310 operations — a 0.6% spread, on the identical shape of test as the two tables above. That is Microcks 1.14.0 on Temurin 21, AMD EPYC 7763 at 16 vCPU, &lt;code&gt;oha&lt;/code&gt; at 256 keep-alive connections, measured 2026-07-30.&lt;/p&gt;

&lt;p&gt;So the gap between Rift and Microcks is almost entirely per-request cost rather than scaling, and it would be dishonest to claim the flatness wedge against it — the argument in this article simply does not apply to Microcks. Microcks is a CNCF project that does a great deal Rift does not: it is API-contract first, it imports OpenAPI and AsyncAPI directly, and it covers async protocols Rift has no answer&lt;br&gt;
for at all. Two engines can share this architecture and differ by a lot for entirely different&lt;br&gt;
reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;None of this is a throughput claim about your workload.&lt;/strong&gt; It is a claim about why the curve is flat. The absolute numbers depend on your hardware, your predicates and your response sizes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where else this shape shows up
&lt;/h3&gt;

&lt;p&gt;The reason I think this is worth reading even if you will never install my project: a per-request loop over a config collection is a slow outage that a team contributes to one commit at a time and&lt;br&gt;
never attributes correctly. The person who adds the entry that tips it over pays nothing. The cost lands on whoever profiles the service six months later, and by then the growth looks like weather.&lt;/p&gt;

&lt;p&gt;Mock servers are one instance. Feature-flag evaluators, authorisation policy engines, routing tables, webhook dispatchers, and validation middleware are all the same shape: N rules, one input, first match wins, N grows monotonically because deleting an entry requires knowing who depended on it.&lt;/p&gt;

&lt;p&gt;If one of yours is on that list, the useful question is not "is it fast" but "what is its cost a function of, and who controls that number." Packet classification is forty years of answers to exactly that question, and almost none of it has made it out of networking.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this is
&lt;/h3&gt;

&lt;p&gt;Rift is a Mountebank-compatible HTTP/HTTPS mock server written in Rust. Compatible meaning the same REST admin API, so existing Mountebank clients work unchanged, and the same &lt;code&gt;imposters.json&lt;/code&gt;, loaded without edits. It is &lt;strong&gt;beta&lt;/strong&gt;, at v0.17.&lt;/p&gt;

&lt;p&gt;That compatibility constraint came before any of the performance work, and it is the reason the matching semantics are Mountebank's rather than mine. Nobody switches mock servers out of dissatisfaction; they switch when switching is free. If adoption requires rewriting your stubs, the benchmark does not matter.&lt;/p&gt;

&lt;p&gt;Rift exists because Mountebank designed a genuinely good API — one worth reimplementing rather than replacing. Its throughput ceiling is a property of Node's single-threaded request handling, which is a runtime cost, not a design flaw, and the design is the part I kept.&lt;/p&gt;

&lt;p&gt;The benchmark harness is in the repo under &lt;code&gt;tests/benchmark&lt;/code&gt;, and it runs the other engines too, not only mine. If you think a comparison here is unfair, that is the fastest way to demonstrate it — and a correction that narrows a gap I have claimed is the most useful thing anyone could send me.&lt;/p&gt;

&lt;p&gt;Before that, though: go and count the stubs in your own config file. In my experience the number is higher than anyone on the team would guess, and nobody has ever measured what it costs.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/achird-labs/rift" rel="noopener noreferrer"&gt;https://github.com/achird-labs/rift&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Matching architecture and full benchmark method:&lt;/strong&gt; &lt;a href="https://achird-labs.github.io/rift/performance/" rel="noopener noreferrer"&gt;https://achird-labs.github.io/rift/performance/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>performance</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
