<?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: Illia Rochev</title>
    <description>The latest articles on DEV Community by Illia Rochev (@illyar80).</description>
    <link>https://dev.to/illyar80</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%2F4026410%2F6435f499-edc9-4184-879e-02003fd0628a.jpg</url>
      <title>DEV Community: Illia Rochev</title>
      <link>https://dev.to/illyar80</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/illyar80"/>
    <language>en</language>
    <item>
      <title>Your integration tests pass, but your message queue still double-processes in production</title>
      <dc:creator>Illia Rochev</dc:creator>
      <pubDate>Sun, 12 Jul 2026 20:32:42 +0000</pubDate>
      <link>https://dev.to/illyar80/your-integration-tests-pass-but-your-message-queue-still-double-processes-in-production-223p</link>
      <guid>https://dev.to/illyar80/your-integration-tests-pass-but-your-message-queue-still-double-processes-in-production-223p</guid>
      <description>&lt;p&gt;You write a consumer, test it locally, everything green. Deploy to staging — green. Then in production, the same message gets processed twice. Orders are duplicated. Notifications fire twice. The team blames "a race condition" and moves on.&lt;/p&gt;

&lt;p&gt;This isn't a bug in your broker. It's a &lt;strong&gt;structural property&lt;/strong&gt; of at-least-once delivery. And it's provable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who needs to care?&lt;/strong&gt; If you write Kafka consumers in production — a backend engineer debugging duplicate orders, a platform team standardizing on at-least-once delivery, or an architect choosing between idempotency, transactional outbox, and exactly-once patterns — this problem affects your system whether or not you've seen evidence of it yet. Integration tests won't find it because they sample the state space. Documentation tells you to make consumers idempotent but does not explain why the crash window is structurally unavoidable, or which mitigations actually hold up under formal analysis. This article gives the formal answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  The crash window
&lt;/h3&gt;

&lt;p&gt;Every at-least-once broker has the same topology:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Consumer:
  1. Receive message
  2. Store result in database
  3. Acknowledge / commit offset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The crash window lives between steps 2 and 3. If the consumer crashes after writing the result but before acknowledging, the broker redelivers. The result is stored twice. Five different brokers — same topology, same crash window.&lt;/p&gt;

&lt;p&gt;This is not a testing gap. Integration tests sample the state space. An interleaving where the crash lands in exactly that window may take millions of runs to hit — or may never hit in staging because production traffic shapes timing differently. Absence of evidence is not evidence of absence.&lt;/p&gt;

&lt;h3&gt;
  
  
  What formal verification adds
&lt;/h3&gt;

&lt;p&gt;We built a verification pipeline that replaces sampling with exhaustion. The approach is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Model the system&lt;/strong&gt; — a formal specification of the consumer-broker interaction with explicit states for store, crash, redeliver, and commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check exhaustively&lt;/strong&gt; — the model checker explores every possible interleaving of events, not just the ones that happen to occur during testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate physically&lt;/strong&gt; — predicted collisions are reproduced with real Docker containers and network fault injection (Toxiproxy)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The pipeline covers three brokers (Kafka, RabbitMQ, NATS JetStream) across four delivery modes — 108 unique configurations in total. Every configuration is checked exhaustively, not sampled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Industry adoption confirms the same finding
&lt;/h3&gt;

&lt;p&gt;Our results are not an academic curiosity. Llingr, a Kafka consumer engine with formal verification at its core, independently discovered the same class of bug during TLA+ model checking of their offset commit protocol. They found a race between CommitTick and partition revocation — a direct variant of the crash window — where an offset could be committed during a revoked window, producing silent duplicate processing. Their verification covered over 1.1 billion distinct states and 4.5 billion transitions across 25 verified properties.&lt;/p&gt;

&lt;p&gt;This is production infrastructure discovering the same structural collision. The problem is real, it's systematic, and it's broker-independent.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the numbers show
&lt;/h3&gt;

&lt;p&gt;Across 108 configurations, the temporal collision is structurally invariant.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Faulty&lt;/td&gt;
&lt;td&gt;Collision (all 27 configs)&lt;/td&gt;
&lt;td&gt;Crash after store before ACK always produces double execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotent&lt;/td&gt;
&lt;td&gt;Pass (18/27 configs)&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;should_skip()&lt;/code&gt; guard prevents redelivery — but only if correctly implemented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2async&lt;/td&gt;
&lt;td&gt;Collision (18/27 configs)&lt;/td&gt;
&lt;td&gt;Async store + commit widens the window, doesn't close it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firefly&lt;/td&gt;
&lt;td&gt;Collision (54/81 configs)&lt;/td&gt;
&lt;td&gt;Pulse-coupled sync does NOT eliminate the crash window&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The firefly result deserves emphasis. Pulse-coupled synchronization (two consumers phase-locking before committing) is a known technique from biological oscillators — fireflies synchronizing their flashes. Applied to Kafka consumers, it fails. The crash window exists within a single consumer's lifecycle: store → crash → redeliver. No amount of phase alignment between consumers can protect an individual consumer's internal store-then-commit sequence. Synchronization governs timing, not atomicity.&lt;/p&gt;

&lt;p&gt;Idempotent consumers (checking &lt;code&gt;should_skip()&lt;/code&gt; before execution) are the only reliable mitigation. There is no architectural trick around this — the crash window is a topological consequence of the at-least-once delivery contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLMs are not the answer (yet)
&lt;/h3&gt;

&lt;p&gt;We tested a GPT-4o-mini predictor on the same 108 configurations (540 calls, temperature 0.7). The result: recall 94.4%, specificity 1.1%, balanced accuracy 47.8% — below random chance. The model defaulted to "yes" 96% of the time. A trivial "always yes" baseline performs identically.&lt;/p&gt;

&lt;p&gt;The role of LLMs in this domain is specification generation, not standalone detection. A formally verified spec checked by an exhaustive model checker is reliable. An LLM judge is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  The practical takeaway
&lt;/h3&gt;

&lt;p&gt;If you run at-least-once consumers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assume every message can be delivered twice&lt;/strong&gt; — because structurally, it can. This is not pessimism, it's a provable property of the delivery contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency is not optional&lt;/strong&gt; — a &lt;code&gt;should_skip()&lt;/code&gt; guard, deduplication key, or exactly-once processing pattern is the only reliable defense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration tests cannot prove absence&lt;/strong&gt; — they sample the state space. Formal verification exhausts it. If your architecture must never double-execute (payments, idempotency keys, inventory), model checking is the appropriate tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The crash window is not a bug. It's a law of distributed systems — like the CAP theorem or the FLP result. You can't fix it. You can only design around it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full methodology and detailed results are available as a preprint (DOI: 10.5281/zenodo.21298530). The toolchain is not publicly available at this time.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>distributedsystems</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
