Skip to content
SeeItFlow
System Design · 12 chapters · 40 incidents · ~88 min read

Distributed Message Queue Failures: Lost Acks, Duplicates & Recovery

Forty hypothetical failure scenarios on a fictional commerce platform, from lost acknowledgments and duplicate deliveries to hot partitions, full disks, leader loss and regional outages. Each is told as a trigger, a timeline, impact, cause, detection, containment, recovery, prevention, the guarantee that still holds and the tradeoff it costs. All numbers are teaching inputs, not measurements.

Mangesh MandlikLast updated: How lessons are made
Chapter 1 · Overview

The Failure Map: Where an Event Can Be When Things Break

Forty ways a message platform goes wrong, from a lost acknowledgment to a missing region, each told as an incident with a clock, a cause, a recovery and the guarantee that survives. The pattern across all of them is that a message passes through several states, and every failure lives at a boundary between two of them.

Every scenario here is hypothetical. They use the fictional company HarborCart (commerce), order HC-2048 with event evt-HC2048-01 on topic orders partition 7 at offset 4,201, a shipment-jobs work queue, consumer groups email-notifier, sales-analytics and fraud-monitor, and a second tenant meridian. The numbers (20,000 events/s average, 100,000 events/s for a ten-minute promotion, 1 KiB events, 24 partitions, replication factor 3, three days of retention) are teaching inputs rather than measurements of any real system.

This page can be read on its own, so the vocabulary first. A producer publishes events; a consumer reads them. A queue hands each message to one worker; a topic delivers each message to every subscription, and a retained log keeps the messages for a while so they can be read again. A partition is one ordered slice of a topic, an offset is a position inside it, and a consumer group shares a topic's partitions among its members so that each partition has one active reader. A leader replica accepts writes for a partition and followers copy it; the in-sync replicas (ISR) are the followers that are keeping up. A lease or visibility timeout hides a claimed job from other workers for a while, and an acknowledgment or offset commit tells the broker the consumer is done. Idempotency means repeating an operation has the same effect as doing it once; a DLQ parks messages that cannot be processed.

Where an event can be, and what each state proves

A published event moves through distinct states: the producer issued it, the broker received it, the leader appended it, the configured replicas hold it, the producer received an acknowledgment, the consumer was handed it, the business effect committed, and the consumer's acknowledgment or offset commit was recorded. These are never the same fact. When the broker acknowledges a publish, it says nothing about whether a consumer has done anything; when a consumer commits an offset, it says nothing about whether a payment was captured unless the code orders them correctly. Most incidents below are the result of treating two adjacent states as one.

What no design on this page can promise

Some guarantees are unavailable at any price, and the incidents keep returning to them: a single global order across a partitioned topic, an effect applied exactly once across systems that share no transaction, replay beyond the retention window, automatic cross-region failover for data that residency rules keep in place, and continued writes after losing more replicas than the policy tolerates. With replication factor 3 and a minimum of two in-sync replicas, one broker can fail and writes continue; two cannot, and writes are refused rather than risk silent loss.

How each incident is written

Each incident starts with the story and the arithmetic where numbers help, then closes with a table listing trigger, timeline, impact, root cause, detection, containment, recovery, prevention, the guarantee that still holds and the tradeoff it costs, plus the invariant (I1 to I12) it touches. The invariants are the ones used across this module: identity comes from the verified principal (I1), a durable acknowledgment follows the advertised persistence (I2), publish, replicate, deliver and effect are separate facts (I3), ordering holds only within a key's partition (I4), there is no free exactly-once (I5), retries are bounded and poison is quarantined (I6), everything is bounded (I7), committed data survives only the failures the policy covers (I8), replay is bounded by retention and authorization (I9), outbox atomicity is local to its database (I10), personal data is minimized (I11) and the key signals are visible (I12).

The map

IDIncident (hypothetical)CategoryChapter
F01Synchronous email timeout fails checkoutSync couplingFrom Synchronous Calls to a Durable Queue
F02Process crashes with queued work in RAMDurabilityFrom Synchronous Calls to a Durable Queue
F03Producer write succeeds but the acknowledgement is lostPublish ambiguityFrom Synchronous Calls to a Durable Queue
F04Broker acknowledges a memory-only write, then crashesDurabilityFrom Synchronous Calls to a Durable Queue
F05Leader fails after replication but before the ackLeader failoverReplication and Leader Failures
F06Leader fails before the record reaches the in-sync replicasLeader failoverReplication and Leader Failures
F07In-sync replica set shrinks below the required minimumAvailabilityReplication and Leader Failures
F08Leader elected from an out-of-date replicaUnsafe electionReplication and Leader Failures
F09Publisher retry duplicates an eventDuplicatesPublisher Retries, Ordering and the Outbox
F10Retries reorder events within a partitionOrderingPublisher Retries, Ordering and the Outbox
F11Consumer applies the side effect, then crashes before committing the offsetCommit orderConsumer Commits, Leases and Rebalances
F12Worker acknowledges the job before the business transaction commitsCommit orderConsumer Commits, Leases and Rebalances
F13Work lease expires while a long job is still runningLeasesConsumer Commits, Leases and Rebalances
F14Rolling deploy rebalances the group while batches are in flightRebalanceConsumer Commits, Leases and Rebalances
F15Orphaned consumer with a stale membership epoch writes anywayFencingConsumer Commits, Leases and Rebalances
F16Poison event blocks a partitionPoisonPoison Events, Retries, DLQs and Schemas
F17Infinite immediate retry creates a hot loopRetryPoison Events, Retries, DLQs and Schemas
F18The dead-letter queue grows silentlyDLQPoison Events, Retries, DLQs and Schemas
F19DLQ redrive repeats a payment captureDLQPoison Events, Retries, DLQs and Schemas
F20A schema change breaks an older consumerSchemaPoison Events, Retries, DLQs and Schemas
F21High-volume customer creates a hot partitionHot partitionLoad, Partitions and Capacity
F22Slow subscriber's backlog grows while other groups are healthyLagLoad, Partitions and Capacity
F23Retention deletes messages before a lagging group catches upRetentionRetention, Replay and Compaction
F24Broker disk fills during a promotion burstDiskLoad, Partitions and Capacity
F25Reconnect storm saturates the metadata and control planeControl planeLoad, Partitions and Capacity
F26Partition expansion changes key placement and ordering scopePartitioningLoad, Partitions and Capacity
F27Order committed, event missing: the dual-write gapOutboxPublisher Retries, Ordering and the Outbox
F28Outbox relay publishes, then crashes before marking rows dispatchedOutboxPublisher Retries, Ordering and the Outbox
F29Clock skew misreports lagMetricsObservability Failures
F30Cross-tenant subscription attemptSecurityTenancy, Payloads and Data Protection
F31A huge or hostile payload overwhelms the validatorAbuseTenancy, Payloads and Data Protection
F32An unauthorized retention override exposes customer dataData protectionTenancy, Payloads and Data Protection
F33A region fails while residency policy forbids mirroringRegionsRegional Failures and Asynchronous Mirrors
F34Async mirror falls behind, then is promoted with a missing suffixRegionsRegional Failures and Asynchronous Mirrors
F35Compaction removes history a consumer assumed was thereCompactionRetention, Replay and Compaction
F36A consumer starts from an offset outside retentionRetentionRetention, Replay and Compaction
F37The metrics pipeline depends on the broken clusterObservabilityObservability Failures
F38A rolling upgrade changes protocol or schema compatibilityCompatibilityPoison Events, Retries, DLQs and Schemas
F39A few extremely heavy keys unbalance partitionsHot keyLoad, Partitions and Capacity
F40Scaling the group beyond the partition count changes nothingScalingLoad, Partitions and Capacity
All 40 incidents and where each is discussed.
F01, F02, F03, F04

From Synchronous Calls to a Durable Queue

The first four incidents follow the order in which HarborCart's design changes: a synchronous checkout that fails because of an optional dependency, a memory-only queue that forgets, a publish whose outcome nobody knows, and an acknowledgment that promises more than the broker has done.

F01 — Synchronous email timeout fails checkout

In HarborCart's original design the checkout handler calls payment, inventory, email and analytics one after another and returns only when all four answer. During a promotion the email provider starts answering in about 8 s instead of the usual few hundred milliseconds. Payment has already captured the money for HC-2048 by then, so the customer sees an error page for an order that was in fact paid.

The arithmetic explains why a slow dependency becomes an outage. Say checkout receives 100 orders/s (a hypothetical promotion rate) and each request now holds a thread for 8 s: Little's Law gives 100 × 8 = 800 requests in flight, against a pool of 200 threads. The pool is full within about two seconds, new requests queue behind it, and the payment and inventory calls that were healthy start timing out purely from waiting. Customers retry, which adds load and risks a second charge unless payment is idempotent. Nothing about email was ever important enough to block a purchase; the chain simply made it so.

Moving email and analytics behind a queue or topic removes them from the request path, but it does not remove the failure; it relocates it. The email consumer can now fall behind quietly, which is why lag and oldest-message age become the signals to watch.

FacetF01 at a glance
TriggerEmail provider latency rises from sub-second to about 8 s while checkout runs at a hypothetical 100 orders/s.
Timeline1) t=0 s email latency climbs to 8 s. 2) t=2 s the 200-thread pool saturates (800 needed). 3) t=5 s payment and inventory calls time out while waiting for threads. 4) t=30 s customers retry; error rate dominates the dashboard.
ImpactPaid orders show failures; duplicate payment attempts; revenue loss on a path that did not need email to complete.
Root causeSynchronous coupling: the slowest optional dependency sets the latency and availability of the whole checkout.
DetectionThread-pool saturation, p99 checkout latency, per-dependency timeout counts; spans showing the email call dominating the trace.
ContainmentSkip or defer email and analytics with a feature flag; shed non-essential callers; keep payment calls idempotent.
RecoveryReconcile orders that were paid but reported failed; send the emails that were skipped from the order table.
PreventionPublish an event after the order commits and let email, analytics and fraud subscribe asynchronously; keep only payment and inventory reservation inline.
Guarantee that remainsThe order and payment still commit atomically with respect to checkout; email delivery becomes eventual and independently retried.
TradeoffAsynchrony buys isolation and costs eventual consistency: the confirmation email may trail the order by seconds or minutes, and there is now a backlog to monitor.
InvariantI7 (everything bounded), I12 (visible lag).
F01: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F02 — Process crashes with queued work in RAM

The first fix HarborCart tried for F01 was an in-process queue: checkout drops a shipment task into a list in memory and a background thread drains it. That is fast and simple, and it is gone the instant the process is. A node is replaced during a routine deploy while 3,000 shipment tasks are waiting.

Swipe to pan
An in-memory queue loses accepted work when the process crashesV1 Trap: Queue in Process MemoryReply was sent before the work was made durable · timeline top to bottomCustomerApp processIn-memory queueWorker thread1 POST HC-20482 push ship-HC-20483 200 OK "accepted"job in RAM only4 process crashes / deploy / OOMqueue gone5 nothing to drainResultThe customer was told "accepted", payment may be captured, and the shipment task silently disappeared. Durability must come before the reply.
A task held only in a process's memory has no second copy; a restart erases it silently.

At a hypothetical working rate of 83.33 tasks/s (300,000 an hour), 3,000 tasks is about 36 s of backlog. Paid orders exist in the database; the shipment jobs that should follow them do not. Nobody receives an error, because the failure is an absence: no exception, no retry, no log line, and the customers are simply not shipped. Discovery comes from support tickets days later unless a reconciliation job compares paid orders to shipments.

A durable queue changes who owns the task. Once the job is stored outside the process, a crash only interrupts a worker, and the leased task becomes available again after its timeout. The remaining question is whether the producer knew the task was stored, which is what F03 and F04 are about.

FacetF02 at a glance
TriggerRolling deploy kills a pod holding 3,000 unprocessed in-memory shipment tasks.
Timeline1) Checkout enqueues tasks into process memory and replies 200. 2) Deploy sends SIGTERM; the drain loop stops with 3,000 tasks left. 3) Pod restarts with an empty queue. 4) Orders stay 'paid' with no shipment record.
ImpactAbout 36 s of shipping work silently lost (3,000 ÷ 83.33); customers wait with no signal.
Root causeThe only copy of the work lived in volatile memory with no acknowledgement boundary.
DetectionPaid-but-unshipped age on a reconciliation query; enqueue count minus processed count diverging; queue depth dropping to zero without work completing.
ContainmentPause deploys; run the reconciliation query to find orders missing shipment rows.
RecoveryRe-create jobs for every paid order lacking a shipment, keyed by order id so reruns are harmless.
PreventionStore jobs in a persistent queue (or write them through the outbox) and acknowledge only after durable storage; make shipment creation idempotent.
Guarantee that remainsOnce the job is durably stored, it is delivered at least once until a worker acknowledges it.
TradeoffA durable queue adds a network hop and a disk write per task, plus a lease or visibility timeout to reason about.
InvariantI2, I3.
F02: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F03 — Producer write succeeds but the acknowledgement is lost

The order service publishes OrderPlaced (event id evt-HC2048-01) to orders. The leader appends it at partition 7, offset 4,201, replicates it, and sends the acknowledgement; the TCP connection resets and the response never reaches the producer. From the producer's side there is only a timeout, and a timeout is a statement about its own knowledge, not about the broker's state.

There are exactly two worlds behind that timeout. In one the record never arrived and no event exists; in the other it is committed and the answer was lost. The producer cannot tell them apart by waiting longer. If it gives up, the order may have no event (the worse case for downstream consumers); if it retries blindly, the log may contain two copies. Within a single producer session an idempotent producer (a client feature that attaches sequence numbers per partition so the broker can drop a repeated batch) turns the retry into a no-op, but only while that session lives.

The robust answer is to treat the outcome as unknown and make both outcomes safe: keep the event in the outbox until a confirmed acknowledgement, resend with the same event id, and let consumers deduplicate on (consumer, event_id).

FacetF03 at a glance
TriggerTCP connection resets after the leader appended offset 4,201 and before the ack reached the order service.
Timeline1) t=0 ms producer sends batch with evt-HC2048-01. 2) t=12 ms leader appends and replicas confirm. 3) t=15 ms ack is sent and dropped by a reset connection. 4) t=15 s producer times out, outcome unknown. 5) t=15 s retry carries the same id; the broker or the consumers recognize the duplicate.
ImpactEither a missing event or a duplicate, depending on how the producer resolves its doubt.
Root causeA publish has three outcomes (confirmed, rejected, unknown) and the unknown case is irreducible over an unreliable network.
DetectionPublish-unknown counter; outbox rows older than a threshold without a confirmation; log-end offsets compared with relay sent counts.
ContainmentDo not mark the outbox row dispatched; retry with the same event id.
RecoveryReconcile by event id; consumers absorb the duplicate through their inbox.
PreventionIdempotent producer within a session, stable event ids across sessions, outbox until confirmed, consumer dedupe. Broker-side dedupe has bounded memory: an SQS FIFO queue, for example, deduplicates sends only within a five-minute interval, so a retry after a longer outage is a new message.
Guarantee that remainsAn acknowledged publish is committed per policy; an unacknowledged one may or may not exist, and the design is correct in both cases.
TradeoffAt-least-once publishing accepts duplicates in exchange for never silently dropping an acknowledged-by-business event.
InvariantI3, I5.
F03: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F04 — Broker acknowledges a memory-only write, then crashes

During a throughput experiment someone sets the shipment-jobs queue to acknowledge as soon as the broker has received the message, with no replication wait and no flush. Publish latency improves visibly. A node crash then reveals what the acknowledgement actually meant.

At 20,000 events/s, a 50 ms window of unflushed data holds 20,000 × 0.05 = 1,000 events (50 ms is a hypothetical flush interval). The broker answered 'got it' for all of them, the producers moved on and marked their outbox rows dispatched, and the crash erased the buffer. Because the producers saw success, nothing retries and nothing alerts. Only a later count of expected versus consumed events shows the gap.

The mistake is not that memory is fast; it is that the acknowledgement claimed more than had happened. A broker acknowledgment can mean any of four different things: received, appended to the log, replicated, or flushed to disk, and a sound design makes each ack name the step it covers. HarborCart's canonical setting waits for the in-sync replicas, so an acknowledged event has already survived a single-broker crash.

FacetF04 at a glance
TriggerAck-on-receive configuration plus a broker crash 30 ms after a batch of acknowledged writes.
Timeline1) Producer sends a batch; the broker buffers it and acks at once. 2) Outbox rows are marked dispatched. 3) The broker process dies before flush and replication. 4) On restart the log ends before the lost batch; nothing triggers a resend.
ImpactRoughly 1,000 acknowledged events vanish per 50 ms of exposure at the average rate; downstream state silently diverges.
Root causeAck semantics weaker than the producer's assumption; the broker reported receipt as if it were commitment.
DetectionEnd-to-end counters (produced vs consumed) per topic; broker restart events correlated with offset gaps; config audit for non-default ack levels.
ContainmentRestore the replicated ack setting; stop marking outbox rows dispatched on weak acks.
RecoveryReplay the affected window from the order outbox or database using event ids; consumers deduplicate.
PreventionTreat durability settings as reviewed production config; wait for the in-sync replicas before acknowledging; test by killing brokers under load.
Guarantee that remainsWith the canonical setting, an acknowledged record is on at least two replicas before the producer is told.
TradeoffWaiting for replication costs publish latency (an illustrative figure of about 15 ms) and some peak throughput.
InvariantI2, I3.
F04: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.
F05, F06, F07, F08

Replication and Leader Failures

A replicated log is only as safe as the rule that says when a write is committed and which replica may lead after a failure. These four incidents sit on the boundary between acknowledged, committed and available.

F05 — Leader fails after replication but before the ack

Broker L leads partition 7 at leader epoch 7. A producer sends OrderPlaced for HC-2048; L appends it at offset 4,201, both followers copy it, and the record is now committed, meaning the in-sync replicas all hold it. L then loses power before it can answer. The controller elects F1, epoch 8, and the record survives on the new leader.

The producer experiences the same silence as in F03 and cannot know any of this. When it retries to the new leader, one of two things happens. If the retry comes from the same idempotent producer session and the sequence state is recoverable from the replicated log, the new leader recognizes the batch and does not append it again. If the retry comes from a freshly started producer, or the relay marked nothing and a new process picks up the row, the log gains a second copy at a later offset. Both outcomes are consistent with at-least-once publishing.

This scenario matters because it shows the commit point and the acknowledgement are different moments. Replication being complete protects the data; the ack only informs the sender. Durability and knowledge can part ways for a few hundred milliseconds, and the design must tolerate that gap.

FacetF05 at a glance
TriggerLeader L loses power between full replication of offset 4,201 and sending the acknowledgement.
Timeline1) L appends offset 4,201 (epoch 7). 2) F1 and F2 copy it; the record is committed. 3) L dies before replying. 4) Controller elects F1; epoch becomes 8. 5) Producer times out and retries against F1.
ImpactNo data loss; a possible duplicate and a few seconds of publish latency or timeout for partition 7.
Root causeThe acknowledgement is sent after commit, so a crash in between separates durability from the sender's knowledge.
DetectionLeader-change events; publish retry spikes; epoch increment on partition 7; duplicate event ids at the consumer inbox.
ContainmentLet the election finish; do not force a second election or restart brokers.
RecoveryVerify end offsets agree across replicas; absorb duplicate event ids through the inbox.
PreventionSame event id on retry; idempotent producer; inbox dedupe; timeouts longer than a normal election.
Guarantee that remainsCommitted records survive the single failure, because the replica set had at least two in-sync copies.
TradeoffA longer client timeout tolerates elections but delays detection of real outages.
InvariantI2, I3, I8.
F05: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F06 — Leader fails before the record reaches the in-sync replicas

Now invert F05. L appends the record at offset 4,201 to its own log, but the followers are still at 4,200 when L crashes. Because the producer asked to wait for the in-sync replicas, no acknowledgement was sent, so the producer never believed it was stored.

F1 becomes leader with its log ending at 4,200. The next event published to partition 7 now receives offset 4,201 on F1's log, which is a different record from the one L wrote. When L restarts it discovers that its tail diverges from the leader of epoch 8 and truncates its extra record before following. The lost record was never acknowledged, so the contract holds, but only if the producer retries. A fire-and-forget producer would have lost an order event without anyone noticing.

Truncation of an uncommitted tail is the mechanism that keeps replicas identical, and it is the reason consumers must read only up to the committed point (the high watermark). A consumer that could see offset 4,201 on L before commit would have read a record that later disappears.

FacetF06 at a glance
TriggerLeader crashes with a locally appended offset 4,201 that no follower has fetched.
Timeline1) L appends offset 4,201; followers lag at 4,200. 2) L crashes; producer receives no ack. 3) F1 is elected (epoch 8) at log end 4,200. 4) Producer retries; the record is committed at a fresh 4,201 on F1. 5) Old L rejoins and truncates its divergent tail.
ImpactOne publish needs a retry; with a careless producer, one event is lost without ever being acknowledged.
Root causeWrites become durable only when the in-sync replicas hold them; before that point a leader's tail is provisional.
DetectionLeader change with uncommitted tail truncation logs; producer error counters; log-end-offset mismatch alarms.
ContainmentProducers retry on timeouts; hold consumers to the committed point.
RecoveryRely on retry from outbox or producer buffers; confirm event id presence after recovery.
PreventionAcknowledge only after the in-sync replicas hold the record; never treat a leader-local append as acceptance.
Guarantee that remainsEvery acknowledged record was on at least two replicas, so a single failure cannot erase it.
TradeoffWaiting for followers adds latency to every publish; a failure can still turn into producer errors that someone must handle.
InvariantI2, I8.
F06: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F07 — In-sync replica set shrinks below the required minimum

orders is configured with replication factor 3 and a minimum of 2 in-sync replicas (the leader counts toward that minimum). A network problem makes F1 and F2 miss their fetch deadlines, and the leader removes them from the in-sync set. The in-sync set is now just L, one replica, below the minimum of two.

Swipe to pan
ISR, minimum ISR and leader failover with RF=3RF=3, minISR=2: one loss keeps writes, two stops themorders partition 7 · producer waits for the in-sync replicas (acks=all equivalent)HealthyISR = 3LleaderF1in syncF2in syncWrites acceptedISR 3 ≥ min 2One broker lostISR = 2LleaderF1downF2in syncWrites still acceptedISR 2 ≥ min 2Two brokers lostISR = 1LleaderF1downF2downWrites rejectedNotEnoughReplicas: 1 < 2committed data survives on L; availability lostLeader failover — the leader itself diesLeader Lepoch 7L crashesheartbeat lostControllerpicks from ISRF1 leadsepoch 8Committed means in every in-sync replicaOnly an in-sync follower may lead, so recordsacked as committed are on the new leader.Unclean election is offAn out-of-sync leader would raise availabilitybut could drop a committed suffix.
Writes stay available while two replicas, including the leader, are in sync; below that, the leader rejects them.

With acknowledgement on in-sync replicas, the leader refuses writes (the Kafka-style error is NotEnoughReplicas) rather than acknowledge something it cannot protect. Data already committed is still there and still readable; what has been lost is the ability to accept new writes on those partitions. If the producers retry for the default delivery timeout of 120 s, the order service keeps committing orders and the outbox grows: 20,000 events/s × 120 s = 2,400,000 rows in two minutes, a number worth having in mind before the relay is asked to catch up.

This is the boundary of the guarantee, not a bug. Replication factor 3 with a minimum of 2 survives one broker loss with writes continuing. Two losses leave committed data on the survivor but stop writes. The alternative, accepting writes on a single replica, would turn the next failure into data loss.

FacetF07 at a glance
TriggerTwo followers fall out of sync after a network partition, leaving an in-sync set of one.
Timeline1) F1 and F2 stop fetching on time. 2) Leader shrinks the in-sync set to {L}. 3) Producers receive NotEnoughReplicas. 4) Outbox rows accumulate at about 20,000 per second. 5) Network heals; followers catch up and rejoin; writes resume.
ImpactWrites to affected partitions are rejected; reads of committed data continue; roughly 2.4 million events wait in the outbox after two minutes.
Root causeThe write policy requires two in-sync replicas and only one remained.
DetectionUnder-min-ISR partition count; ISR shrink events; producer error rate; outbox row count and oldest unsent age.
ContainmentKeep rejecting writes; fix the network or the follower; expand relay capacity only after writes work.
RecoveryFollowers rejoin after catching up; the relay drains the outbox with event ids intact.
PreventionCapacity for followers, network redundancy, alerts on ISR shrink before it reaches the minimum.
Guarantee that remainsCommitted data remains on the surviving replica; no acknowledged write was accepted under the minimum.
TradeoffRejecting writes costs availability to protect durability; the choice to flip it (minimum of 1) trades loss risk for uptime.
InvariantI7, I8.
F07: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F08 — Leader elected from an out-of-date replica

Unclean leader election is disabled on orders. In the middle of an incident, with L and F1 both down, an operator sees partition 7 offline and enables it temporarily so that service resumes. F2 had been lagging by 1,200 records, so it becomes leader with a log that ends 1,200 offsets earlier than the committed end.

Writes resume at once, which looks like success. Every one of those 1,200 records had been acknowledged to producers, and they now exist only on the offline brokers. When those brokers return they must truncate to follow the new leader, so the records are erased. Consumers whose committed offsets sit beyond the new end find themselves holding positions that no longer exist; depending on policy they reset to earliest or latest, and either one can skip or reprocess data.

This is the cleanest example of availability bought with durability. The setting is not wrong in every context (a metrics topic may accept it), but for orders the cost is invisible loss of acknowledged events. The recoverable path is to bring a complete replica back, even if it takes minutes longer.

FacetF08 at a glance
TriggerOperator enables unclean election during a double broker outage; the elected replica is 1,200 records behind.
Timeline1) L and F1 fail; partition 7 has no in-sync leader. 2) Operator enables unclean election. 3) F2 becomes leader (epoch 8) with a shorter log. 4) Writes resume; L and F1 return and truncate 1,200 records. 5) Consumers see offsets out of range.
Impact1,200 acknowledged records are lost; consumers reset or fail; order state diverges from the event stream.
Root causeElection preferred availability over the committed suffix.
DetectionUnclean election log lines and metric; offset out-of-range errors; consumer resets; produced versus consumed audits.
ContainmentPrefer waiting for a full replica; revert the override immediately after the election.
RecoveryRebuild the lost window from the order database and outbox, replaying events by id; consumers deduplicate.
PreventionKeep unclean election off for business topics; add replicas or racks so a double failure is rarer; require review for overrides.
Guarantee that remainsWith the setting off, an elected leader always holds the committed suffix.
TradeoffSafety may mean an offline partition until a complete replica returns.
InvariantI8.
F08: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.
F09, F10, F27, F28

Publisher Retries, Ordering and the Outbox

Retrying a publish is how producers survive a broken network, and it is also how duplicates and reordering get in. The outbox solves a different gap, between the order database and the broker, and has its own duplicate.

F09 — Publisher retry duplicates an event

HarborCart's outbox relay reads evt-HC2048-01, publishes it, and the batch is appended at offset 4,201. The relay's call times out locally, the relay process exits on a health-check failure, and the replacement starts a new producer. It reads the same outbox row, still not marked, and publishes again; the log now holds the event at 4,201 and again at some later offset.

The broker did exactly what it was asked. An idempotent producer protects a single session (a producer id plus its epoch, with per-partition sequence numbers), so a new process is a stranger to the broker. Application-level resends with new identifiers are also invisible to it. The protection belongs to the layer that knows the business identity of the event: the consumer's inbox keyed by (consumer, event_id).

A common reaction is to chase an exactly-once publisher. Transactions in a log can make read-process-write cycles atomic inside the log, yet they do not extend to a payment API or a mail provider. The honest design accepts duplicates and removes their effect.

FacetF09 at a glance
TriggerRelay timeout and process replacement cause the same outbox row to be published by two producer sessions.
Timeline1) Relay publishes evt-HC2048-01; broker appends it. 2) Local timeout fires and the process is replaced. 3) New relay session finds the row unmarked. 4) Event is appended a second time.
ImpactTwo log copies of one business event; duplicate emails or analytics rows unless consumers deduplicate.
Root causeProducer idempotence is scoped to one session; identity across sessions has to come from the application.
DetectionDuplicate event-id counts at the inbox; relay restart count; outbox rows with more than one publish attempt.
ContainmentLet the inbox reject duplicates; avoid manual republishing.
RecoveryNo repair needed if the inbox exists; otherwise correct downstream aggregates by event id.
PreventionStable event ids, an inbox table, idempotent side effects.
Guarantee that remainsEach consumer applies the event at most once as long as the inbox write and effect share a transaction.
TradeoffThe inbox costs a lookup and storage per event and a retention policy for dedupe keys.
InvariantI5.
F09: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F10 — Retries reorder events within a partition

The order service sends OrderPlaced and OrderPaid for HC-2048 with key order:HC-2048, so both go to partition 7. The producer allows several batches in flight and has idempotence turned off. The batch holding OrderPlaced fails with a transient error, the one holding OrderPaid succeeds at offset 4,201, and the retried OrderPlaced lands at 4,202.

The ordering promise was only ever about append order, and the append order has been changed by the retry. A consumer sees a payment for an order it has not yet seen. Depending on code it may drop the update, create a stub order, or crash into the retry path. Strict ordering is a property of the whole chain: key choice, in-flight limit, idempotence, and consumer logic.

With idempotence on, a Kafka-style producer can keep up to five requests in flight and still preserve order per partition, because the sequence numbers let the broker reject out-of-order retries. Setting a single in-flight request also works but caps throughput. Ordering across partitions is simply not promised.

FacetF10 at a glance
TriggerIdempotence disabled with several in-flight batches; a transient failure on the first batch.
Timeline1) Producer sends batch A (OrderPlaced) and batch B (OrderPaid). 2) A fails transiently; B succeeds at offset 4,201. 3) A is retried and appended at 4,202. 4) Consumer reads OrderPaid before OrderPlaced.
ImpactState machine violations: payment before placement; consumer errors and rework.
Root causeRetry plus multiple in-flight batches without sequence checks changes append order.
DetectionPer-key sequence checks at consumers; out-of-order transition counters; producer retry rates.
ContainmentRoute out-of-order events to a parking path with a short wait for the missing predecessor.
RecoveryReprocess in the correct order after the predecessor arrives; correct state by version number.
PreventionEnable idempotence with the in-flight limit it requires; use per-key version numbers; design consumers to tolerate a short reorder window.
Guarantee that remainsWithin one partition, with idempotence, order of acknowledged appends is preserved.
TradeoffLow in-flight limits protect order and give up peak throughput; consumer-side buffering costs state and delay.
InvariantI4.
F10: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F27 — Order committed, event missing: the dual-write gap

The order service writes the order row for HC-2048 to its database, then publishes OrderPlaced to the broker, as two independent steps. The database commit succeeds and the process crashes before the publish. The customer sees a confirmed order; email, fraud and analytics never hear of it.

A database transaction and a broker publish cannot be made atomic by ordering the calls. If you publish first and the commit fails, you broadcast an order that does not exist; if you commit first and the publish fails, you lose the event. Retrying inside the same process helps only until the process dies, which is exactly when it matters.

The transactional outbox removes the second write: the order row and an outbox row commit in one local transaction, and a relay publishes the outbox row later, marking it dispatched on a confirmed acknowledgement. The state of affairs after a crash is recoverable because the intent is stored. This atomicity is local to the originating database; it does not make the downstream consumers' effects atomic with anything.

FacetF27 at a glance
TriggerProcess crash between database commit and broker publish.
Timeline1) Order HC-2048 commits. 2) Process crashes before publish. 3) Customer sees a confirmation. 4) No OrderPlaced event is ever produced.
ImpactOrders with no email, no fraud screening, no analytics; silent inconsistency.
Root causeTwo systems written separately cannot be committed atomically.
DetectionOrders without matching events (reconciliation by id); outbox lag; business KPI gaps.
ContainmentRun a reconciliation job to find unpublished orders.
RecoveryPublish missing events from the order table with their stable ids.
PreventionTransactional outbox with an at-least-once relay and idempotent consumers; see the outbox pattern.
Guarantee that remainsIf the order row exists, the outbox row exists; the event will be published at least once.
TradeoffAn outbox adds a table, a relay and publish latency, and requires consumer dedupe.
InvariantI10, I5.
F27: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F28 — Outbox relay publishes, then crashes before marking rows dispatched

The relay reads 500 undispatched outbox rows, publishes them as a batch, and is about to update their status when it is killed. The broker holds all 500 events. The database still lists them as pending.

Swipe to pan
Relay crash after publish yields the same event twice; the inbox absorbs itRelay Crash After Publish, Before Mark-SentDuplicates are expected by design; the same event ID is what makes them harmlessOutbox rowRelayorders topicConsumer inbox1 read evt-HC2048-01 (unsent)2 publishappend @4,2013 ACK4 crash before "mark sent"5 restart: row still unsent6 publish againappend @4,2xx dup7 deliver both1st applied · 2nd skippedReading the pictureThe topic holds two records of one event ID; the log is faithful, not deduplicated. Exactly-once effect = at-least-once publish + idempotent consumer.
The relay may crash between publishing and marking; the same event id is then published again.

On restart the relay selects the same 500 rows and publishes them again. The log now has 500 duplicates, adjacent to the originals or later depending on timing. Per-key order is still intact for the first copy of each event; the second copies arrive after newer events, so a consumer that treats arrival order as truth sees an old status after a new one. The relay is at-least-once by construction: the only way to make it at-most-once is to risk F27's loss.

Consumers protect themselves with the inbox and, for state, version numbers (apply an event only if its version is newer than the stored one). Smaller relay batches reduce the number of duplicates after a crash at the cost of more database round trips; neither choice removes them.

FacetF28 at a glance
TriggerRelay killed after a successful publish of 500 rows and before the dispatched update.
Timeline1) Relay selects 500 pending rows. 2) Broker acknowledges the batch. 3) Relay dies before marking them. 4) Restarted relay publishes the same 500 again.
ImpactUp to 500 duplicate events per crash, with possible stale-after-new ordering at consumers.
Root causePublish and mark are two steps; crash between them repeats the publish.
DetectionDuplicate event id rate at inboxes; relay restarts; relay batch retry counts.
ContainmentDo not manually mark rows; let consumers deduplicate.
RecoveryNo repair needed with idempotent consumers; otherwise correct aggregates by event id.
PreventionInbox, version checks, stable ids, relay batch sizing.
Guarantee that remainsEvery committed order's event is published at least once.
TradeoffSmaller batches reduce the duplicate window and add database load.
InvariantI5, I10.
F28: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.
F11, F12, F13, F14, F15

Consumer Commits, Leases and Rebalances

Consumers are where a delivery becomes a business effect. These incidents are about the order of the effect, the acknowledgment and the ownership claim, and what happens when ownership changes under a running worker.

F11 — Consumer applies the side effect, then crashes before committing the offset

The sales-analytics group reads OrderPlaced for HC-2048 from partition 7, offset 4,201. It inserts a revenue row into the analytics database, and the process is killed by the OOM killer before it commits offset 4,201. The committed offset, which is the group's bookmark of work finished, still says 4,200. (Kafka actually stores the next offset to read, so its bookmark would read 4,201 here; this page writes the last finished offset to keep the story easy to follow.)

Swipe to pan
Where a consumer crash lands relative to the DB write and offset commitCrash Matrix: Side Effect vs Offset Commitsales-analytics consuming evt-HC2048-01 · lightning bolt = crash windowA · commit offset firstreadcommit offsetDB write⚡Effect LOSTat-most-once: redelivery never comesB · DB write, then commit offsetreadDB writecommit offset⚡Effect DUPLICATEDat-least-once: record is read againC · B + inbox in same DB txreadDB tx: inbox + rowcommit offset⚡Effect ONCEredelivery hits inbox unique key → skippedCrash window = between steps 2 and 3 (above the ⚡). In C the offset commit may still be redone; the inbox, not the offset, guards the effect.Scope: C works for effects in that one DB. A call to an external payment API needs its own idempotency key.
Crash points around the side effect and the commit decide whether an event is lost, repeated, or applied once.

When the partition is reassigned, the new owner resumes at the bookmark and reads 4,201 again. A second revenue row appears. Nothing was malfunctioning: the group committed after the side effect, which is the safe order, because the opposite order loses data (F12). It means redelivery is an expected event and a consumer that is not ready for it double-counts. Revenue dashboards drift upward by one order at a time, which is a hard error to notice by eye.

The repair belongs inside the database transaction. The consumer writes the business row and an inbox row (sales-analytics, evt-HC2048-01) together; a replay hits the unique key and is ignored. The offset commit then merely records progress, and the inbox is what carries correctness. If the effect is not in a database the consumer controls (an email, say), the effect itself needs an idempotency key supported by the provider.

FacetF11 at a glance
TriggerProcess killed between the analytics insert for offset 4,201 and the offset commit.
Timeline1) Consumer polls offset 4,201. 2) Insert into the analytics table commits. 3) Process dies before the offset commit. 4) New owner resumes from committed offset 4,200. 5) Event 4,201 is delivered again.
ImpactOne duplicate revenue row per crash per in-flight event; totals drift.
Root causeThe side effect and the offset commit are two separate writes; no crash-safe ordering makes both atomic.
DetectionDuplicate event-id counts; reconciliation of analytics revenue against the order database; redelivery rate after restarts.
ContainmentPause the consumer if the duplicate rate is high; do not rewind blindly.
RecoveryDelete duplicate rows by event id; add the inbox then replay.
PreventionCommit after the effect; write an inbox row in the same transaction as the effect; use unique keys.
Guarantee that remainsWith the inbox, each event's effect is applied once per consumer even though delivery is at-least-once.
TradeoffAn inbox table adds a write per event and retention work for old ids.
InvariantI5, I3.
F11: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F12 — Worker acknowledges the job before the business transaction commits

Worker A claims ship-HC-2048 from the shipment-jobs queue. To keep the queue clear, its code acknowledges the message as soon as it is received and then starts creating the shipment row in the order database. The database connection pool is exhausted, the transaction fails, and the process exits.

The queue believes the work is done: the ack told it to delete the message. There is no lease to expire and no retry to trigger. The task has vanished while the order remains paid and unshipped. This is the mirror image of F11, and unlike it nothing will ever repair itself. A DLQ cannot catch it either, since the message was never negatively acknowledged. RabbitMQ's own documentation describes the same trap for automatic acknowledgement mode, where a delivery counts as done as soon as it is sent, so a consumer whose connection or channel closes first loses the message.

The rule for an acknowledgement is that it asserts the work is finished or safely handed to something durable. Commit the transaction first, then ack; accept that a crash in the gap yields a redelivery, and make the work idempotent by job id (the shipment table's unique key on ship-HC-2048).

FacetF12 at a glance
TriggerWorker acks on receipt; the database transaction later fails and the process exits.
Timeline1) Worker A leases ship-HC-2048. 2) Worker A acks immediately. 3) Database pool exhausted; transaction fails. 4) Process exits; the job no longer exists in the queue.
ImpactA paid order is never shipped; no alert fires because the queue shows empty and healthy.
Root causeAcknowledgement was sent before the effect, so the broker's record of completion is false.
DetectionPaid-but-unshipped age; enqueued minus effects-committed counters; ack-before-commit found in a code review or a trace.
ContainmentFix the worker to ack after commit; find orders without shipment rows.
RecoveryRecreate the missing jobs from the order table with job ids, relying on the unique key.
PreventionAck after the transaction commits; lease timeouts; a reconciliation job as a safety net.
Guarantee that remainsIf a worker ack follows a committed effect, the job cannot be lost; a crash before the ack causes redelivery.
TradeoffAck-after-commit trades loss for possible duplicate work, which the shipment key absorbs.
InvariantI3, I5.
F12: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F13 — Work lease expires while a long job is still running

A work queue hides a claimed message from other workers for a visibility timeout (also called a lease): HarborCart uses 60 s as an illustrative value. Worker A claims ship-HC-2048, which includes generating a customs document that normally takes seconds and today takes 95 s because a partner service is slow.

Swipe to pan
Visibility lease, missed renewal and redeliveryA lease is a bet that the worker is aliveJob ship-HC-2048 on shipment-jobs · 60 s lease (illustrative) · seconds on the axisQueue stateWorker AWorker Bt=40 renewal due — missedlease expiresinvisible · leased to Aleased to Bdeletedprocessing×stalls (pause or crash) — no heartbeatlate ack ✗stale receipt rejectedattempt 2✓0 s20 s40 s60 s80 s100 sWhat the lease cannot knowA slow worker looks identical to a dead one. If A was onlypaused, both A and B may now act on the same job.What keeps this safeHeartbeat renewals sized to the work, a bounded attemptcount, and an idempotent job effect keyed by job id.
A lease that is shorter than the job hands the same task to a second worker while the first is still running.

At t=60 s the lease lapses and the queue makes the job visible again. Worker B claims it and begins its own 95 s run. For roughly 35 s both workers work on the same job, and both attempt to create a shipment record and print a label. Each did precisely what the system allows; the queue assumed 60 s was enough.

Three defences work together. Workers extend the lease with a heartbeat while they are alive and making progress, so a slow job is not mistaken for a dead one. The shipment table's unique key on the job id makes the second creation fail harmlessly. And a fencing token (a number that increases at each claim) lets the table reject a write from the stale claimant. Setting the lease to an enormous value is the tempting shortcut and it delays recovery from a genuinely crashed worker by the same amount. Real products expose the same dial under different names: Amazon SQS hides a received message for a visibility timeout (30 seconds by default, at most 12 hours from the receive) and lets a worker extend it with ChangeMessageVisibility; a RabbitMQ quorum queue returns a delivery to the queue if the consumer holds it unacknowledged past a delivery acknowledgement timeout (30 minutes by default in current documentation) or closes its channel. In both, a consumer that outlives its claim can still be running, which is why the fence and the unique key matter.

FacetF13 at a glance
TriggerJob duration of 95 s exceeds the 60 s visibility timeout.
Timeline1) t=0 s Worker A leases ship-HC-2048. 2) t=60 s lease expires; the job is visible again. 3) t=61 s Worker B claims it. 4) t=95 s A finishes and writes a shipment row. 5) t=156 s B finishes and hits the unique key, or writes a duplicate.
ImpactDouble processing for about 35 s; duplicate labels or two shipments if the effect is not idempotent.
Root causeLease duration was shorter than the real service time; the broker cannot tell slow from dead.
DetectionRedelivery count per job; job duration histogram against the lease; duplicate shipment constraint violations.
ContainmentRaise the lease for the slow class of jobs; let the unique key stop duplicates.
RecoveryCancel duplicate labels and shipments found by the job id.
PreventionLease heartbeat, duration sized to p99 plus margin, idempotent effects, fencing token.
Guarantee that remainsAt most one claim holds a valid lease at a time, though an expired claimant may still be running.
TradeoffHeartbeats add traffic and complexity; long leases slow crash recovery.
InvariantI5, I6.
F13: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F14 — Rolling deploy rebalances the group while batches are in flight

The email-notifier group has 12 consumers and 24 partitions, two partitions each. A rolling deploy restarts the consumers one by one. Each restart triggers a rebalance, which is the group coordinator redistributing partition ownership, and the group generation advances from 12 to 13 and onward. With an eager protocol, every member gives up all its partitions at each rebalance; with a cooperative protocol only the partitions that must move are revoked.

Swipe to pan
24 partitions assigned to 12 consumers; one crashes and the group rebalancesConsumer Group Assignment and Rebalanceemail-notifier on orders · 24 partitions · each partition has one owner per group at a timeGeneration 12 · 12 consumers × 2 partitionsC1p0p1C2p2p3C3p4p5C4p6p7C5p8p9C6p10p11C7p12p13C8p14p15C9p16p17C10p18p19C11p20p21C12p22p23✕ crashcoordinator detects missed heartbeats → generation 13 → revoke / assignGeneration 13 · sticky / cooperative: only p22, p23 moveC1p0p1p22C2p2p3p23C3p4p5C4p6p7C5p8p9C6p10p11C7p12p13C8p14p15C9p16p17C10p18p19C11p20p2111 consumers: 24 = 2×11 + 2, so a balanced result hastwo owners with 3 partitions and nine with 2. Sticky keepsthe move to 2 partitions; the 3-vs-2 skew must be planned.A consumer beyond partition 24 would sit idle: parallelismis capped by partitions, not consumers. Uncommitted workof C12 is redelivered to the new owner (dedup needed).
A rebalance reassigns partitions; offsets not yet committed are re-read by the new owner.

The duplicate arises in the gap. A consumer holds a polled batch (offsets 4,201 to 4,300 on partition 7) and has already sent 40 emails. The rebalance revokes the partition before the offset is committed and hands it to another member, which restarts at the last commit and sends those 40 emails again. For a short period two members may be handling the same offsets, one finishing, one beginning.

Cooperative rebalancing reduces the work interrupted: when one of twelve consumers dies, 24 partitions over 11 survivors means two take three partitions and nine keep two, and a sticky assignor moves only the two orphans. Static membership (each consumer keeps a fixed instance id, so a quick restart within the session timeout does not trigger a rebalance at all) is the other lever for rolling deploys. Handling revocation by committing progress for finished records narrows the window; only an inbox or an idempotency key at the email provider closes it.

FacetF14 at a glance
TriggerRolling deploy of 12 consumers advances the group generation while batches are in flight.
Timeline1) Consumer C3 polls offsets 4,201 to 4,300. 2) C3 has sent 40 emails. 3) A deploy restart triggers a rebalance; generation 12 becomes 13. 4) Partition 7 moves to C5, which resumes at 4,201. 5) C5 sends the same 40 emails again.
ImpactCustomers receive duplicate emails; downstream rate limits may trip.
Root causeOwnership moved before progress was committed; processing and ownership are not atomic.
DetectionRebalance count and duration per deploy; duplicate event ids at the email provider; commit failures after revocation.
ContainmentSlow the deploy rate; pause between restarts until the group is stable.
RecoverySuppress or apologize for duplicates by event id; no state to repair.
PreventionCooperative assignment, commit on revocation, inbox dedupe keyed by event id, provider-side idempotency.
Guarantee that remainsEach partition has one owner per generation; duplicates are possible across a generation change.
TradeoffCooperative protocols are more complex and slower to converge; smaller batches reduce the duplicate window but cost throughput.
InvariantI5, I6.
F14: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F15 — Orphaned consumer with a stale membership epoch writes anyway

Consumer C3 in sales-analytics hits a 50 s garbage-collection pause. The coordinator, hearing no heartbeat past the session timeout, declares C3 dead and gives its partitions to C5 at generation 13. C3 resumes still believing it owns partition 7 at generation 12.

C3's attempt to commit its offset is rejected, because the commit names an old generation; the broker fences it. That is good, and it is only half of the protection. The database insert that C3 issues before committing carries no generation at all, so the analytics table accepts it. C5 is by now processing the same offsets. The two write paths race, and the later write may overwrite a correct row with a stale one.

The offset fence guards the log, not your database. To protect external state, carry the generation (or any monotonically increasing claim number) into the write: UPDATE ... WHERE owner_generation <= :mine, or store it with the row and refuse anything lower. The same idea appears as a fencing token for locks and as the lease fence in F13.

FacetF15 at a glance
TriggerA 50 s pause outlasts the session timeout; ownership moves while the old owner is still alive.
Timeline1) C3 pauses for 50 s. 2) Coordinator evicts C3; partition 7 goes to C5 (generation 13). 3) C3 wakes with generation 12 and continues its batch. 4) Offset commit is fenced; database writes are not. 5) C3 and C5 both write rows.
ImpactStale or duplicate writes to analytics; possible overwrite of newer state.
Root causeFencing exists at the broker for commits but not at the external datastore.
DetectionFenced-commit error counts; generation mismatches logged on write; unusual duplicate or out-of-order versions.
ContainmentRestart the paused member; temporarily lower batch sizes.
RecoveryRebuild affected rows from the log by replaying the partition for the window.
PreventionFence database writes with the generation or per-row version; keep the time between polls shorter than the maximum poll interval and tune the session timeout to the longest realistic pause.
Guarantee that remainsA fenced member cannot commit offsets for a partition it no longer owns.
TradeoffPer-write fencing adds a column and conditional update; shorter timeouts cause more false evictions.
InvariantI4, I5.
F15: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.
F16, F17, F18, F19, F20, F38

Poison Events, Retries, DLQs and Schemas

Failures that repeat deterministically need different handling from ones that pass. Here the subject is classification, bounded retries, parking, redrive and the schema changes that create poison events.

F16 — Poison event blocks a partition

A consumer reads partition 3 and meets an OrderPlaced whose payload fails parsing, because a bug in a producer wrote a truncated JSON body. The consumer throws, does not commit, and the next poll reads the same offset. In a log, one consumer owns a partition and reads strictly forward, so the group cannot skip it without an explicit decision.

The average rate on one partition is 833.3 events/s (20,000 over 24). While the consumer loops, a backlog grows by that much: after 10 minutes the partition is 500,000 events behind. Every key that hashes to that partition is delayed, which is 1/24 of traffic, while the other 23 partitions look fine and the aggregate lag graph hides the problem in an average.

The guard is a classification step. A permanent failure (the bytes cannot ever parse) goes to the dead-letter topic after a bounded number of attempts, with the offset committed past it, and the consumer continues. A transient failure (a timeout) is retried with backoff. Parking the event also breaks ordering for that key until someone decides what to do, and an operator must know which events are waiting.

FacetF16 at a glance
TriggerOne malformed record at partition 3 fails parsing on every attempt.
Timeline1) Consumer polls the malformed record and throws. 2) Offset is not committed; the same record is re-read. 3) Backlog on partition 3 grows at 833.3/s. 4) After 10 minutes the lag is about 500,000 events. 5) Alert fires on one partition's age.
Impact1/24 of keys stall; per-partition lag climbs while the aggregate stays moderate.
Root causeNo distinction between permanent and transient errors; the loop retries forever on a strictly ordered stream.
DetectionPer-partition lag and oldest message age; repeated exception on the same offset; retry counters.
ContainmentSkip to the dead-letter topic by hand or deploy the classification fix.
RecoveryFix the producer, correct the payload, and redrive from the DLQ after review.
PreventionSchema validation on write, error classification, bounded attempts (max 5), DLQ with an alert.
Guarantee that remainsOther partitions are unaffected; order on the affected partition holds up to the parked record.
TradeoffSkipping a record trades strict per-key order for liveness; someone must handle the parked event.
InvariantI6, I4.
F16: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F17 — Infinite immediate retry creates a hot loop

The fraud-monitor consumer calls an inventory service that starts returning 503. The code catches the error and retries immediately in a tight loop with no limit. Each of the 12 consumers does this about 100 times per second, so a service that was already struggling absorbs 1,200 extra calls per second (hypothetical loop speed) at the moment it needs relief.

This is an amplification: the failing dependency's load rises because it is failing. Meanwhile the consumers make no progress, lag grows, and when the dependency recovers it faces the retry storm plus the real backlog together. Without jitter, consumers that failed together retry together, so the load arrives in synchronized waves.

Bounded retry with exponential backoff and jitter spreads the load: 2 s, 4 s, 8 s, 16 s plus random variation, at most 5 attempts, then the dead-letter path. A circuit breaker gives the dependency room to recover, and a retry budget limits the fraction of traffic that is retries. See retry with backoff and the retry storm module for the general mechanism.

FacetF17 at a glance
TriggerInventory returns 503; consumers retry immediately without limit.
Timeline1) Inventory begins failing. 2) Consumers retry in a tight loop at about 100/s each. 3) Inventory sees 1,200 extra requests per second. 4) Recovery is delayed; lag climbs. 5) Backlog and retries hit the dependency together when it returns.
ImpactLonger outage of the dependency; consumer lag; wasted capacity.
Root causeRetry without backoff, jitter, a limit or a breaker.
DetectionRetry rate to the dependency; ratio of retries to first attempts; consumer lag; synchronized spikes in the dependency's request graph.
ContainmentTurn on the breaker and cap attempts; temporarily pause the group.
RecoveryResume gradually; let the backlog drain at a rate the dependency tolerates.
PreventionBackoff with jitter, attempt cap, breaker, retry budget, DLQ.
Guarantee that remainsEach event is attempted a bounded number of times before it is parked.
TradeoffLonger backoff delays recovery of transient errors; parked events need handling.
InvariantI6, I7.
F17: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F18 — The dead-letter queue grows silently

After a schema mistake, about 40 events per minute (hypothetical) are sent to orders.dlq, which retains data for 14 days. The consumer group is healthy, lag is zero, and every dashboard is green, because parking an event counts as a success for the main pipeline.

The numbers accumulate quietly: 40 × 60 × 24 = 57,600 a day, and after 14 days 806,400 events. On day 15 the retention policy starts removing the oldest parked events and those orders are permanently gone, never examined. A DLQ is itself a queue with its own retention, so unattended it is only a slower way to lose messages.

In Amazon SQS the clock is even less forgiving: a message's expiry is measured from its original enqueue time and is not reset when it moves to the dead-letter queue, so AWS advises giving the DLQ a longer retention period than the source queue. Treat the DLQ as an operational inbox. Alert on depth, arrival rate, and oldest age (say, a page when the oldest is above 24 hours); tag each entry with the failure reason, original topic, partition, offset and attempt count; and give someone an owner and a runbook.

FacetF18 at a glance
TriggerA steady trickle of failing events is parked and nobody watches the DLQ.
Timeline1) A producer change causes 40 events/min to fail validation. 2) Events move to orders.dlq; main lag stays at zero. 3) 57,600 per day accumulate. 4) Day 15: retention begins deleting the oldest parked events.
ImpactOrders never processed; eventual permanent loss of the oldest parked events.
Root causeThe DLQ had no owner, alert, or review cadence.
DetectionDLQ depth, arrival rate, oldest-message age; ratio of parked to consumed events.
ContainmentAlert and triage immediately; extend DLQ retention while investigating.
RecoveryFix the cause, then redrive with review (see F19) and idempotent processing.
PreventionAlerts, owner, runbook, reason metadata, retention longer than the review cycle.
Guarantee that remainsA parked message is retained for the DLQ retention period and not beyond it.
TradeoffLonger retention costs storage and may hold personal data longer.
InvariantI6, I12.
F18: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F19 — DLQ redrive repeats a payment capture

A payment-capture job times out five times against the card processor and is parked. In reality the processor captured the funds on the first call and only the response was slow. HarborCart's incident shows 300 such parked jobs (hypothetical) after a processor brownout. An engineer bulk-redrives them into the main topic, and 300 customers are charged twice.

Swipe to pan
Bounded retries, poison parking and DLQ redriveRetry a little, then park the poisonMax 5 attempts, backoff 2 → 4 → 8 → 16 s with jitter (illustrative)shipment-jobsready jobsWorkerattempt nClassify failuretransient / permanentSuccess → ackoktransientpermanent (bad schema, rule violation) → park nowAttempt 1+2 sAttempt 2+4 sAttempt 3+8 sAttempt 4+16 sAttempt 5Dead-letter queueretained 14 dEach wait returns the job to the queue with jitter so a burst of failures does not retry in lockstep.Human-reviewed redrivepayment topicsredrive → shipment-jobs, rate-limitedRules that keep the queue healthyClassify first: retrying a permanent failure only burns attempts.Park with the error, attempt count and original headers; alert on DLQ depth.Redrive after the fix, at a bounded rate, so parked jobs cannot become a storm.
Retries lead to a DLQ; redrive sends parked events back and may change their relative order.

Parking an event does not prove the side effect did not happen. A timeout is the same unknown outcome as in F03. Redriving also reorders: the redriven job reaches the consumer after later events for the same order, so a refund for HC-2048 can be processed before the capture it refers to. FIFO guarantees, where they exist, apply to the original delivery within a key, not to the moment an operator reinjects a message.

For payment-capturing topics redrive is human-reviewed. Before reinjection, query the processor by idempotency key, send the idempotency key with each capture so that a repeat returns the earlier result, and redrive in small batches with the consumer's inbox in place. Where order matters, redrive to a separate topic or hold later events for the same key until the redrive completes.

FacetF19 at a glance
TriggerBulk redrive of 300 parked payment jobs whose captures had actually succeeded.
Timeline1) Processor slows; capture calls time out. 2) After 5 attempts the jobs move to the DLQ. 3) Operator redrives all 300. 4) The consumer calls the processor again. 5) Without an idempotency key, 300 customers are charged twice.
Impact300 double charges; refunds, support load and trust loss.
Root causeRedrive assumed that parked meant not executed; the effect had no idempotency key.
DetectionDuplicate captures by order id; processor-side duplicate alerts; reconciliation of captures against orders.
ContainmentHalt redrive immediately; freeze the topic.
RecoveryRefund duplicates, identify them by idempotency key, and reconcile ledgers.
PreventionIdempotency keys at the processor, review before redrive, small batches, status checks, redrive into a controlled path.
Guarantee that remainsWith idempotency keys, a repeated capture returns the earlier result instead of charging again.
TradeoffHuman review slows recovery; idempotency requires the processor's support.
InvariantI5, I6.
F19: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F20 — A schema change breaks an older consumer

The OrderPlaced contract is on version 2 and fraud-monitor still reads v2. A producer team ships v3, renaming the required total field to totalAmount, through a hotfix path that skipped the registry's compatibility check. The registry is set to BACKWARD compatibility, meaning a new reader schema must be able to read data written under the old schema; that mode is there to make consumers upgrade first.

Swipe to pan
Schema compatibility matrixCompatibility is checked before the first recordBACKWARD: new readers read old data · FORWARD: old readers read new dataSchema change to OrderPlacedBACKWARDFORWARDWhyAdd optional field (with default)✓✓safe for both directionsAdd required field (no default)✗✓new readers cannot fill old dataRemove optional field✓✓safe for both directionsRemove required field✓✗old readers still expect itRename a field (no alias)✗✗looks like remove + addChange type (int → string)✗✗neither side can decodeRegistry gate on the publish path (BACKWARD enforced on OrderPlaced)Producerbuilds v3Schema registrycheck vs v2Rejected at registerCompatible → publishExample: v3renames total → amount:incompatible, fix first
A rename is not backward compatible; the registry rejects it before any producer can emit it.

Here the producer went first. fraud-monitor deserializes the new records, finds no total, and treats it as 0, so every order now looks harmless to the fraud rules. Nothing crashes, which is worse than a crash. Other consumers fail loudly and park events by the thousand. The log is retained for 3 days, so poisoned events are recoverable but cannot be unwritten.

Compatibility has to be checked before the schema is accepted, enforced in CI and at runtime registration, with a non-bypassable path. Changes that break must go through a new topic or version, with a dual-write migration period. The check cannot prove semantic compatibility (a field that keeps its name but changes meaning).

FacetF20 at a glance
Triggerv3 publishes totalAmount in place of total after a skipped compatibility check.
Timeline1) Hotfix deploys a v3 producer. 2) fraud-monitor (v2) finds no total field and defaults it to 0. 3) Risk scores are computed on 0. 4) Other consumers fail validation and park events. 5) Rollback begins.
ImpactWrong fraud decisions; parked events; consumer errors.
Root causeThe compatibility gate was bypassed and the rollout order was producers first.
DetectionDeserialization errors, null-rate on required fields, schema registration audit, DLQ arrivals.
ContainmentRoll the producer back; pause the affected consumers.
RecoveryReplay affected orders after reprocessing with corrected code; review decisions made on bad data.
PreventionRegistry with BACKWARD checks in CI, consumers upgraded first, no bypass, contract tests.
Guarantee that remainsRegistered schemas are compatible with the configured mode; semantic changes are outside that proof.
TradeoffCompatibility rules slow change and force dual versions during migrations.
InvariantI7, I12.
F20: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F38 — A rolling upgrade changes protocol or schema compatibility

A new OrderPlaced v3 producer rolls out region by region, while consumers upgrade over a week. Halfway through, the schema registry accepts v3 because someone set the subject's compatibility to NONE for convenience. Old consumers that still decode with v2 begin failing on records from upgraded producers, and failed events retry, then flow to orders.dlq.

Swipe to pan
Schema compatibility matrixCompatibility is checked before the first recordBACKWARD: new readers read old data · FORWARD: old readers read new dataSchema change to OrderPlacedBACKWARDFORWARDWhyAdd optional field (with default)✓✓safe for both directionsAdd required field (no default)✗✓new readers cannot fill old dataRemove optional field✓✓safe for both directionsRemove required field✓✗old readers still expect itRename a field (no alias)✗✗looks like remove + addChange type (int → string)✗✗neither side can decodeRegistry gate on the publish path (BACKWARD enforced on OrderPlaced)Producerbuilds v3Schema registrycheck vs v2Rejected at registerCompatible → publishExample: v3renames total → amount:incompatible, fix first
Compatibility mode decides whether a new schema is accepted; upgrade order must match it.

There are two layers. The wire protocol between clients and brokers has its own version negotiation, and a request using a version the other side does not support is refused with an explicit unsupported-version error. That is visible and loud. Schema compatibility is quieter: the data parses at the transport layer and breaks in the application. With BACKWARD compatibility, new schemas can read old data, which suits consumers upgraded first. Upgrade order matters: consumers before producers for backward changes, producers before consumers for forward ones.

A rolling upgrade should be a series of reversible steps with explicit gates: upgrade brokers, then clients, then flip the new schema or feature on only when the lowest-version member of every fleet understands it.

FacetF38 at a glance
TriggerRegistry compatibility relaxed to NONE; v3 producers deployed before consumers.
Timeline1) Subject compatibility set to NONE. 2) v3 producers deploy in EU. 3) Old consumers fail to decode; retries start. 4) Poison-like failures spill into the DLQ.
ImpactFailed processing for upgraded events; DLQ growth; possible delay in email and analytics.
Root causeA compatibility gate removed, and rollout order not aligned with the compatibility direction.
DetectionDecode-error rate per consumer version, registry config-change audit, DLQ growth.
ContainmentRoll the producers back or fall back to v2 serialization; pause redrive.
RecoveryUpgrade consumers, then redrive parked events after review.
PreventionKeep BACKWARD (or stricter) compatibility; run contract tests with real old consumers; version-aware rollout gates.
Guarantee that remainsWhile compatibility is enforced, upgraded readers handle old data and the registry rejects breaking changes.
TradeoffStrict compatibility slows schema evolution and forces additive changes.
InvariantI7, I6.
F38: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

Redrive, ordering and what a DLQ does not prove

Parking an event in a dead-letter queue records that processing failed, not that nothing happened. The consumer may have charged the card and then failed on the confirmation email, so a parked payment event is potentially a completed payment. That is why orders.dlq redrive for payment-capturing topics is human-reviewed, and why every consumer on the redrive path needs the same inbox check as on the live path: the event id decides whether the effect already exists.

Ordering is a second trap. A partition delivers its events in order to its single consumer, and that is the whole ordering promise. Once an event is parked, its successors for the same key continue on the main path, and when the parked event is later redriven it arrives after them, so OrderPaid may be processed before the redriven OrderPlaced. Redrive therefore cannot restore the original sequence; consumers need version numbers or state checks to tolerate it, or a stricter design pauses the whole key until the poison event is resolved. Queue-style FIFO guarantees are narrower still: strict first-in first-out breaks as soon as there are several consumers, retries or redeliveries, and is meaningful only inside one ordering scope such as a message group.

F21, F22, F24, F25, F26, F39, F40

Load, Partitions and Capacity

Capacity problems in a log are mostly about where load lands. Hot partitions, idle consumers, slow groups, full disks and reconnect storms each need a different lever.

F21 — High-volume customer creates a hot partition

A partition is the unit of parallelism and of ordering: records with the same key go to the same one, and one consumer in a group owns it. Someone chooses tenant as the key, reasoning that per-tenant order is desirable. HarborCart's own harbor-retail tenant is 30% of the traffic.

Swipe to pan
A tenant-only key sends one tenant's traffic to a single partitionHot Partition from Key Design24 partitions · 20,000 events/s average · harbor-retail = 30% (illustrative)Bad key: tenant=harbor-retailuniform833/sp3 ≥ 6,000/s7.2 × uniformp0p3p23others ≈ 609/s each (14,000 ÷ 23)EffectOne partition has one active owner in a group, soits consumer saturates and lag grows while the other23 partitions have spare capacity. More consumers cannot help.Fix, and its priceKey by order:<orderId>: tenant load spreads out.Price: no order across a tenant's orders. Orderingis kept where the business needs it: per order.
One key funnelled into one partition leaves one partition overloaded and the rest with spare capacity however many consumers exist.

At 20,000 events/s, 30% is 6,000 events/s, all of it hashed to a single partition, against a uniform share of 833.3 events/s (20,000 over 24). That partition receives 7.2 times its fair share, one broker's disk and one consumer's CPU absorb it, while the other 23 partitions run at about 609 events/s each (14,000 over 23), well within what a partition can carry. Adding consumers does nothing, since a partition has one owner at a time. Adding partitions does not help either, because the tenant is still one key.

The canonical key is order:<orderId>, which spreads a tenant's orders while keeping each order's events in sequence. The wider lesson is to pick the narrowest key that still carries the ordering the business actually needs. When a single entity really is that hot, split it with a salt (order:<id>:<n>) and accept that ordering applies within each salted lane, or move the heavy tenant to a dedicated topic with its own partitions.

FacetF21 at a glance
TriggerTenant-only partition key while one tenant produces 30% of events.
Timeline1) Producer hashes tenant to partition 3. 2) Partition 3 receives 6,000 events/s; others receive about 600 to 833. 3) The owning consumer saturates and lag builds on partition 3. 4) Adding consumers changes nothing.
ImpactLag and latency on every event of the busy tenant; broker and consumer imbalance.
Root causeKey cardinality too low for the load; ordering scope was set wider than needed.
DetectionPer-partition bytes in and lag; max-to-mean partition ratio (7.2 here); one partition's consumer at 100% CPU.
ContainmentRaise that consumer's capacity; apply a tenant quota; temporarily shed low-priority events.
RecoveryCreate a new topic with the correct key and migrate with dual publishing and consumer cutover.
PreventionChoose order:<id> as key; monitor key skew; tenant quotas.
Guarantee that remainsOrder within each key is preserved; across keys there is none.
TradeoffA finer key shrinks the ordering scope; a salt multiplies lanes that must be merged in order.
InvariantI4, I7.
F21: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F22 — Slow subscriber's backlog grows while other groups are healthy

orders has three independent consumer groups (named sets of consumers that share the partitions of a topic so each record is processed once per group). email-notifier and fraud-monitor are healthy. sales-analytics slows down because its database is under maintenance, and it manages 12,000 events/s against an arrival rate of 20,000.

Swipe to pan
Consumer lag versus oldest event ageLag counts records; oldest age counts painBoth are needed: they diverge when one partition is stuck (illustrative HarborCart inputs)A · Outage, then catch-up36,000,000 events · oldest 30 mindrain 30,000/s(50,000 − 20,000 in)consumers downdrained0102030405060minutesbacklogB · One partition stucklag: only a handful of eventsoldest age keeps climbinga poison record is blocking itspartition; the rest looks calm0102030405060minutesIf the recovering group only matches the incoming rate (20,000/s) the backlog never drainsAlert on both signals: lag in events for capacity, oldest age for the user-visible delay, and per-partition views for stuck records.
Backlog in records and in age are different signals; age maps directly to business impact.

Its backlog grows by 8,000 events/s: after 30 minutes, 14,400,000 events. In time terms the oldest unprocessed record is 14,400,000 ÷ 20,000 = 720 s, 12 minutes, old. Because each group keeps its own offsets, the slow group does not slow the other two, which is the whole point of the log over a shared queue. Retention, however, ignores consumers: the broker deletes by age or size whether or not the slow group has read the data, so a slow group is not a claim on disk but a race against the retention clock, and the end of that race is F23.

Lag measured in offsets and lag measured in age tell different stories. Offsets say the group is 14.4 million behind; age says 12 minutes. The second is what a business owner understands. Alert on both, and keep the answer to a lagging group proportionate: scale its consumers if partitions allow, fix the downstream, or accept lag for a non-urgent view such as analytics.

FacetF22 at a glance
Triggersales-analytics throughput falls to 12,000 events/s against 20,000 arriving.
Timeline1) Analytics DB slows; consumers cannot keep pace. 2) Backlog grows 8,000/s. 3) After 30 min: 14.4 million events behind, oldest record 12 min old. 4) Other groups remain current.
ImpactStale analytics; no effect on email or fraud.
Root causePer-group throughput below the arrival rate; groups are independent by design.
DetectionGroup lag by partition; oldest message age; consumer throughput versus arrival rate.
ContainmentReduce downstream pressure; scale consumers up to the partition count; pause non-critical replays.
RecoveryDrain at a rate above arrival; with 50,000/s against 20,000 arriving, 14.4 million drains in 480 s.
PreventionCapacity planning per group, lag alerts, independent scaling.
Guarantee that remainsIsolation: one group's lag does not delay another group's delivery.
TradeoffEach independent group reads the log separately, so fan-out multiplies read bandwidth.
InvariantI6, I12.
F22: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F24 — Broker disk fills during a promotion burst

The promotion pushes ingest to 100,000 events/s for 10 minutes, five times the 20,000 baseline. At 1 KiB each, that is 97.66 MiB/s and 57.22 GiB gross over 600 s; the amount above the baseline is 45.78 GiB. With RF=3, every extra byte is stored three times. A broker that had less headroom than its share of that growth reaches a full disk, because time-based retention cannot free anything younger than 3 days.

Swipe to pan
Backpressure across service stagesEvery stage is bounded, so pressure can travel upstreamA saturated database becomes a rejected publish only through visible, bounded stepsProducersend buffer20% fullProducerbounded send bufferblock briefly, thenfail or shedGatewaytenant quota30% fullGatewayper-tenant quotareply 429 + retryhint; never queuewithout a boundBrokerdisk78% fullBrokerdisk high-watermarkretention bounds thelog; reject or throttlewrites at the limitConsumersin-flight cap100% fullConsumerscap in-flight workpause fetching whendownstream slowsDownstream DBconn pool100% fullDownstreamthe pool is the reallimit; lag is onlythe symptombackpressure travels upstream
Backpressure should appear at each stage before storage runs out, not only at the broker.

Once a log directory fills, the broker stops being able to append and may take the affected partitions offline. Leaders move to the replicas, which hold the same data and may be nearly as full. The failure then cascades: failover adds load to survivors, replicas fall out of sync, and writes begin to be rejected (F07). Producers retry and the outbox grows.

The remedy starts before the disk: producers need a bounded buffer and a load shedding policy (drop or delay low-priority events), consumers need backpressure signals, and operators need an alert on projected time to full, using the rate of growth rather than a fixed percentage. Size for peak plus failure, not for the average, and decide in advance what is shed first.

FacetF24 at a glance
TriggerA 10-minute 5x burst adds 45.78 GiB above baseline (57.22 GiB gross) before retention can reclaim anything.
Timeline1) Burst starts at 100,000 events/s. 2) Disks grow at roughly three times the ingest rate across replicas. 3) One broker's disk fills; its partitions go offline. 4) Leadership moves; survivors take extra load. 5) Writes begin to be rejected.
ImpactRejected writes, outbox growth, delayed events, risk of cascading failure.
Root causeNo headroom for burst and no backpressure; retention could not free space.
DetectionDisk usage and growth rate; time-to-full projection; ISR shrink; producer error rate.
ContainmentShed low-priority topics; extend capacity; temporarily reduce retention on non-critical topics.
RecoveryFree space, let followers catch up, and drain the outbox.
PreventionCapacity for the peak and a failed node; quotas; backpressure to producers; retention by bytes as a safety net.
Guarantee that remainsRecords already committed remain durable; new writes are bounded by policy.
TradeoffHeadroom costs money that mostly sits idle; shedding gives up some events deliberately.
InvariantI7, I8.
F24: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F25 — Reconnect storm saturates the metadata and control plane

A five-minute network blip drops the connections of 600 producers and 150 consumers (hypothetical fleet). They all reconnect within the same second, each requesting cluster metadata for 24 partitions, and the group coordinator receives join requests from every consumer at once.

The data path might survive, but the control path is a smaller, shared resource. Metadata requests queue, rebalances begin and restart as members time out, and the group enters a rebalance loop in which nobody processes records. The incident is self-inflicted: clients behave the same way at the same time because they run the same code with the same timeouts.

The remedy is jitter and limits on the client side (randomized reconnect backoff with a cap), server-side rate limits on metadata requests, longer session timeouts relative to blip duration, static membership for stable consumers, and separating the control plane's capacity from the data plane's. Rolling restarts should be paced too, since a deploy is a controlled version of the same storm.

FacetF25 at a glance
TriggerSimultaneous reconnect of 750 clients after a five-minute network interruption.
Timeline1) Network returns. 2) All clients reconnect and request metadata within one second. 3) Coordinator and broker request queues fill. 4) Members time out and rejoin; rebalances repeat. 5) Throughput is near zero for minutes.
ImpactGroup-wide stall even though brokers are healthy.
Root causeSynchronized client behavior overloaded a small shared control plane.
DetectionMetadata request rate, rebalance count and duration, join request latency, connection count spikes.
ContainmentThrottle connections at a load balancer; stop auto-restarting failing clients.
RecoveryLet the groups stabilize after the storm; restart in batches if necessary.
PreventionJittered backoff, metadata caching, quotas, static membership, bulkheads for control traffic.
Guarantee that remainsCommitted data and offsets remain intact; only progress stalls.
TradeoffJitter and slower reconnects lengthen recovery from short blips.
InvariantI7, I12.
F25: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F26 — Partition expansion changes key placement and ordering scope

To prepare for the promotion, the platform team grows orders from 24 to 48 partitions. Key-to-partition placement uses a hash modulo the partition count, so the mapping changes for roughly half the keys: a key stays only if its hash modulo 48 equals its hash modulo 24.

Take order:HC-2048. Its earlier events, OrderPlaced at partition 7, offset 4,201 and OrderPaid at 4,202, are on partition 7. A later OrderShipped hashes to partition 31, which is owned by a different consumer who may process it before the first consumer has finished the older events. For the period around the change, a per-key order that was guaranteed is no longer guaranteed, and the existing history cannot be moved without rewriting the log.

Expansion therefore is a planned migration. Quiesce or drain before expanding where order is essential; use the new partitions for new topics; or version the key with the partition count and have consumers tolerate a transition window. Expanding is also one-way (partition counts normally cannot shrink), so the cost of over-provisioning early is lower than the cost of expanding late.

FacetF26 at a glance
TriggerIncrease of partitions from 24 to 48 while events for open orders are in flight.
Timeline1) Operator doubles partitions. 2) About half of the keys now map to a new partition. 3) OrderShipped for HC-2048 goes to partition 31. 4) Consumer of 31 processes it before the earlier events on 7.
ImpactOut-of-order processing for about half of the keys during the transition; state-machine errors.
Root causePlacement depends on the partition count; ordering is a per-partition property.
DetectionPer-key sequence gaps; out-of-order transition metrics; partition-count change audit.
ContainmentPause consumers briefly or buffer by version; avoid further changes.
RecoveryReprocess affected keys in version order; compare state against the order database.
PreventionChoose partition counts with headroom for peak (more than 50 for the hypothetical peak); plan expansions with a drain window.
Guarantee that remainsAfter the change, ordering is again per key within the new partition.
TradeoffOver-partitioning costs memory, file handles and rebalance time; under-partitioning forces risky expansions.
InvariantI4.
F26: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F39 — A few extremely heavy keys unbalance partitions

order:<id> keys spread HarborCart's load well in normal times, but a different topic keyed by sku carries one flash-sale item whose key produces 6,000 events/s, and a second hot SKU hashes to the same partition. That partition receives 12,000 events/s while the 24-partition uniform average is 833.3 events/s, about fourteen times higher. Under the hypothetical per-consumer capacity of 2,000 events/s, the owner of that partition is six times overloaded and lags without bound, however many consumers exist.

Unlike F21, where a whole tenant was concentrated, here the hot units are individual keys, and there are only a few. Adding partitions doesn't split one key; both keys will still map to some partition each (possibly the same one). Adding consumers doesn't help either, as a partition has a single owner within a group.

Real options are all about the key. Salt the hot key into several sub-keys (sku#0 to sku#7) and accept that order is then preserved only within each sub-key, or route hot keys to a dedicated topic, or handle the ordering requirement downstream. The first question is whether strict ordering of that key's events matters at all.

FacetF39 at a glance
TriggerTwo heavy SKUs hash to one partition: 12,000 events/s versus 833.3 uniform average.
Timeline1) Flash sale starts. 2) The hot partition's lag grows while others stay near zero. 3) Scaling the group has no effect. 4) On-call identifies the keys by sampling.
ImpactUnbounded lag for the entire partition, including unrelated keys that share it.
Root causeKey skew: a unit of ordering that exceeds one consumer's capacity.
DetectionPer-partition bytes and lag skew; top-N keys by volume; one partition's lag diverging from the rest.
ContainmentDivert the hot keys to a dedicated topic or salt them; shed non-critical consumption for that partition.
RecoveryDrain the backlog with temporary parallel consumers only where ordering is not required; verify results.
PreventionChoose keys with enough cardinality, monitor skew, plan salting for known hot entities, test with realistic key distributions.
Guarantee that remainsEvents for a given key remain ordered within one partition (unless salted).
TradeoffSpreading a key across sub-keys buys throughput and gives up total ordering for that key.
InvariantI4, I7.
F39: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F40 — Scaling the group beyond the partition count changes nothing

The sales-analytics group is behind during a promotion. It has 24 consumers for 24 partitions, each with the hypothetical 2,000 events/s capacity, so the group tops out at 48,000 events/s against 100,000 arriving. The team scales to 40 consumers. Sixteen of them join, receive no partitions, and sit idle. Throughput stays 48,000 events/s, the bill grows, and lag keeps growing at 52,000 events/s.

Swipe to pan
24 partitions assigned to 12 consumers; one crashes and the group rebalancesConsumer Group Assignment and Rebalanceemail-notifier on orders · 24 partitions · each partition has one owner per group at a timeGeneration 12 · 12 consumers × 2 partitionsC1p0p1C2p2p3C3p4p5C4p6p7C5p8p9C6p10p11C7p12p13C8p14p15C9p16p17C10p18p19C11p20p21C12p22p23✕ crashcoordinator detects missed heartbeats → generation 13 → revoke / assignGeneration 13 · sticky / cooperative: only p22, p23 moveC1p0p1p22C2p2p3p23C3p4p5C4p6p7C5p8p9C6p10p11C7p12p13C8p14p15C9p16p17C10p18p19C11p20p2111 consumers: 24 = 2×11 + 2, so a balanced result hastwo owners with 3 partitions and nine with 2. Sticky keepsthe move to 2 partitions; the 3-vs-2 skew must be planned.A consumer beyond partition 24 would sit idle: parallelismis capped by partitions, not consumers. Uncommitted workof C12 is redelivered to the new owner (dedup needed).
Partitions, not consumers, set the ceiling: extra consumers beyond the partition count receive nothing.

In a Kafka-style group each partition has exactly one owner at a time, so the partition count is the upper bound on parallel consumption. Extra members act as standbys that can take over if an active one fails. To process 100,000/s at 2,000 per consumer you would need 50 partitions' worth of owners, so the real choices are more partitions, faster consumers, or accepting lag until the burst ends.

Raising the partition count has side effects (F26): key placement changes and ordering across the transition is not preserved, so it is a planned operation rather than an emergency lever. A queue with competing consumers on one shared list, such as the shipment-jobs work queue, behaves differently, since workers are not bound to partitions.

FacetF40 at a glance
Trigger24 partitions; group scaled from 24 to 40 consumers during a 100,000 events/s burst.
Timeline1) Burst begins; lag climbs at 52,000 events/s. 2) Team adds 16 consumers. 3) Rebalance completes; 16 members hold no partitions. 4) Lag keeps growing.
ImpactNo reduction in lag; extra cost; a needless rebalance.
Root causeParallelism is capped by partition count in a group-based log.
DetectionActive versus idle member counts; per-member assigned partitions; lag slope unchanged after scaling.
ContainmentReduce the group back; make downstream work cheaper; shed low-priority processing.
RecoveryPlan partition expansion with the key-placement consequences understood; drain the backlog when the burst ends.
PreventionSize partitions for peak parallelism ahead of time, with headroom; measure per-consumer capacity; document the ceiling.
Guarantee that remainsEach partition is consumed by one group member at a time, so per-partition order is kept.
TradeoffMore partitions raise parallelism and also metadata, file handles and rebalance cost.
InvariantI4, I7.
F40: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.
F23, F35, F36

Retention, Replay and Compaction

A retained log promises replay only within limits. These three incidents show consumers that assumed more history than the log was configured to keep.

F23 — Retention deletes messages before a lagging group catches up

The log keeps records for 3 days by time, regardless of whether anyone has read them. A deploy accidentally leaves sales-analytics paused. Nobody sees it over a long weekend, and 4 days pass.

By the third day the oldest unread offsets cross the retention boundary and the broker deletes whole segments from the log's start. When the group restarts, its committed offset points at data that no longer exists, an out-of-range error. The reset policy decides what happens: earliest jumps to the oldest retained record and silently skips what was deleted, while latest jumps to the end and skips everything. One day of data at 20,000 events/s is 1,728,000,000 events, permanently missing for that group, though other groups that kept up have it processed.

Committed offsets are also subject to expiry once a group is empty for a long time (the default is 7 days in recent Kafka versions), so a long outage can lose the bookmark itself. The defense is to alert on lag age against retention (for example, page at 50% of retention), keep retention longer than the longest tolerable outage including weekends, and have a rebuild path from the source of truth.

FacetF23 at a glance
TriggerA paused group stays idle for 4 days against a 3-day retention.
Timeline1) Group stops on day 0. 2) Day 3: oldest unread segments become eligible for deletion. 3) Day 4: group restarts; committed offset is out of range. 4) Reset policy skips the missing day.
ImpactMissing analytics for about 1.73 billion events (1 day); wrong totals unless rebuilt.
Root causeRetention is time-based and independent of consumer progress.
DetectionLag age as a percentage of retention; out-of-range errors; consumer group state empty for long.
ContainmentExtend retention for the topic now, before more segments expire.
RecoveryRebuild the gap from the order database or snapshots; replay what remains.
PreventionRetention longer than worst outage; alerts at fractions of retention; runbook for restart.
Guarantee that remainsReplay is bounded by retention; the log never promised more.
TradeoffLonger retention costs disk and extends the time sensitive data is held.
InvariantI9, I12.
F23: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F35 — Compaction removes history a consumer assumed was there

The customer-address topic is compacted: the broker keeps at least the latest record for each key and eventually discards older ones. A new analytics job starts from the beginning of the topic expecting every address change since launch, so that it can build an address-history report. What it reads is the latest address per customer, plus whichever older values had not yet been cleaned.

Compaction is a state-snapshot feature, not an archive. It lets a late consumer rebuild current state without replaying every update, and in exchange gives up intermediate values. Even for the latest value there is a subtlety: a delete is recorded as a tombstone (a record with a null value), which is itself removed after delete.retention.ms (24 hours by default). A consumer that is offline longer than that can miss that the key was deleted.

The report built on that data looked plausible, which is the real danger: no error, just missing history. The fix is to put history where it belongs. Keep a retained, non-compacted event topic, or an event-sourced store or warehouse, for audits, and let the compacted topic feed only the current-state view.

FacetF35 at a glance
TriggerAnalytics job assumes a compacted topic contains the full change history.
Timeline1) Job starts at the earliest offset. 2) It reads only surviving records, mostly one per key. 3) Report shows one address per customer. 4) Finance asks why history is missing.
ImpactSilently incomplete analytics and audit results.
Root causeMisunderstanding the retention semantics of a compacted topic.
DetectionRecord-count and key-cardinality comparison against the source of truth; reconciliation of reports.
ContainmentWithdraw the report; mark the topic's semantic in its catalog entry.
RecoveryRebuild history from the database change log or a non-compacted topic where one exists.
PreventionDeclare cleanup policy and its meaning in the topic catalog; use a separate retained topic for history.
Guarantee that remainsA compacted topic retains at least the latest value per key (until a tombstone expires).
TradeoffCompaction bounds storage and speeds state rebuild, but intermediate states are not recoverable.
InvariantI9.
F35: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F36 — A consumer starts from an offset outside retention

The fraud-monitor group is paused for a long weekend of maintenance, for 5 days. Retention on orders is 3 days, so the oldest 2 days of what it still needed has been deleted. On restart the group asks to resume from offset 3,100 on partition 7; the log now starts at 3,980. The broker reports the position is out of range.

What happens next is a configuration decision someone made long ago: the reset policy. With 'earliest', the group jumps to the oldest available record and silently skips roughly 880 events on that partition. With 'latest', it jumps to the end of the log and skips everything not yet read, and with 'none' it fails loudly and waits for a human. The wrong default on a fraud consumer is worse than the outage.

For a consumer whose misses are costly, 'none' is the safer default: the group stops, and an operator chooses the restart point deliberately (an offset looked up by timestamp, or the oldest retained record plus a backfill job for the gap). The expiry of committed offsets described in F23 applies here as well; a group empty for longer than the offsets retention period would not even have offset 3,100 to ask for.

FacetF36 at a glance
TriggerGroup down for 5 days against 3-day retention.
Timeline1) Group stops for planned maintenance. 2) Two days of its unread data age out. 3) Group restarts and requests offset 3,100. 4) Broker reports out-of-range; reset policy applies.
ImpactMissed fraud signals for the deleted range, or a stalled group, depending on the policy.
Root causeDowntime exceeded retention, and the recovery behavior was left on a default.
DetectionOffset-out-of-range errors; alert on lag as a fraction of retention (consumer-lag-to-retention ratio).
ContainmentStop the group; set reset behavior deliberately; assess the missed range.
RecoveryRebuild state from the system of record or an archive; backfill the fraud model with a batch job.
PreventionAlert when lag approaches 50% of retention; extend retention for critical groups; archive to cheaper storage; choose reset policy explicitly.
Guarantee that remainsReplay is possible only inside retention and authorization limits.
TradeoffLonger retention costs storage across three replicas; archives add another system to operate.
InvariantI9, I12.
F36: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.
F30, F31, F32

Tenancy, Payloads and Data Protection

A shared platform needs identity, bounded inputs and controlled configuration. Each of these incidents is a trust boundary that was drawn in the wrong place.

F30 — Cross-tenant subscription attempt

A second tenant, meridian, runs a legacy integration against HarborCart's platform. Its subscription request carries a tenantId field in the request body, and the legacy gateway trusts it. A bug (or an attacker) sets it to harbor-retail. The gateway authorizes the subscription and starts delivering harbor-retail orders to Meridian's client.

The failing assumption was that the tenant is something the caller says. Identity has to come from the credential: the authenticated principal maps to a tenant, and topic ACLs grant that principal specific operations on specific topics. A payload field can describe the data; it cannot authorize access to it. In this scenario, about 1,000 events (hypothetical) were delivered before an alert, including customer addresses.

Containment means revoking the credential and listing every delivered offset. Correct enforcement checks the ACL on the verified principal at subscribe time and on every fetch, logs denials, and treats a denial pattern as a security signal. Quotas keep a single tenant from using the shared cluster as an exfiltration channel.

FacetF30 at a glance
TriggerSubscribe request names another tenant in its payload and the gateway trusts it.
Timeline1) Meridian client sends tenantId=harbor-retail. 2) Legacy gateway authorizes by payload. 3) Events begin flowing to Meridian. 4) Anomaly alert fires after about 1,000 events.
ImpactExposure of another tenant's order data; regulatory and trust consequences.
Root causeAuthorization derived from untrusted input instead of the verified principal.
DetectionACL denials and grants audit; unusual consumer group on a restricted topic; bytes read per principal.
ContainmentRevoke the credential; block the group; snapshot logs.
RecoveryNotify per policy; rotate credentials; reconcile exposed offsets.
PreventionTopic ACLs bound to the verified principal, deny by default, per-tenant quotas, audit.
Guarantee that remainsWith ACLs on the principal, a caller can read only topics it is explicitly granted.
TradeoffFine-grained ACLs add operational overhead and onboarding steps.
InvariantI1.
F30: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F31 — A huge or hostile payload overwhelms the validator

HarborCart caps a single event at 1 MiB (an illustrative limit) while the average order event is 1 KiB, so the cap is 1,024 times the typical size. A compromised integration for tenant meridian starts publishing 1 MiB documents full of deeply nested JSON, at 400 requests/s. That is about 400 MiB/s of inbound traffic, roughly twenty times the entire 19.53 MiB/s average ingest of the platform, and each payload makes the schema validator do real parsing work before the broker can reject or accept it.

The harm arrives from two directions. Network and CPU on the ingest tier are consumed first, so legitimate producers see timeouts. Anything that does get through is replicated three times and held for three days, so a few minutes of abuse leaves a long tail of useless bytes on disk. A size check that runs after full deserialization protects nothing, because the expensive step has already happened.

The defense is ordered cheapest-first: reject on the declared length before reading the body, apply a per-principal byte-rate quota, then validate with a bounded parser (depth and field-count limits) and only then append. Quota rejections should be explicit and retryable so a well-behaved client backs off rather than hammering.

FacetF31 at a glance
TriggerOne tenant sends 1 MiB nested documents at 400 requests/s against a 1 KiB average and a 1 MiB cap.
Timeline1) Requests arrive carrying large declared lengths. 2) Ingest nodes read and parse full bodies before checking size policy. 3) CPU and bandwidth saturate; healthy producers time out. 4) Accepted payloads are replicated and retained.
ImpactPublish latency spikes for every tenant; wasted storage and replication bandwidth; possible spillover to unrelated topics.
Root causeResource limits applied after the costly work instead of before it, and no per-principal byte-rate bound.
DetectionBytes in per principal, rejected-request counts, p99 of validation time, histogram of payload sizes with a sudden upper tail.
ContainmentThrottle or suspend the offending principal; lower the effective cap temporarily; keep other tenants' quotas intact.
RecoveryPurge or tombstone the oversized records if policy allows, rotate the credential, and review the integration.
PreventionReject on declared length early, per-principal byte and request quotas, bounded parser depth, schema checks before append.
Guarantee that remainsAccepted records fit the declared envelope and caps; one principal cannot exceed its quota.
TradeoffTight limits will reject some legitimate large documents; those need a claim-check pattern (store the blob elsewhere, publish a reference).
InvariantI7 (everything bounded), I1.
F31: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F32 — An unauthorized retention override exposes customer data

Retention on orders is three days, and each event includes shipping addresses. An engineer debugging a replay request sets the topic's retention to unlimited from a shared admin console that did not require a specific permission. Weeks later a security review finds roughly a month of address data still sitting in the log, readable by every group that is allowed to consume orders, and past the period the privacy policy promised.

Two separate mistakes combined. First, configuration was a privilege that nobody had to justify: altering topic settings is a control-plane action that deserves its own ACL and audit record. Second, the log was holding personal data in plain form. An append-only log cannot easily delete one customer's record, so deletion rules are best implemented by limiting what goes in (redact or tokenize before append), encrypting sensitive fields with per-subject keys that can be destroyed, and keeping retention short by policy.

After discovery the immediate step is to restore the intended retention; segments past it are removed on the next cleanup. Removing earlier copies that were mirrored or replayed into other systems is a separate hunt, because the log is not the only place that data lives.

FacetF32 at a glance
TriggerRetention on a PII-bearing topic raised to unlimited without approval.
Timeline1) Engineer needs old events for a replay. 2) Admin console permits the config change. 3) Retention stays unlimited for about a month. 4) Audit finds address data beyond the stated window.
ImpactPrivacy-policy breach; larger blast radius for any later read-access mistake; unplanned storage growth.
Root causeTopic configuration not protected by a dedicated ACL and change review; sensitive fields stored unredacted.
DetectionConfig-change audit log, drift detection comparing live topic config to declared config, storage growth on a single topic.
ContainmentReset retention to the declared value; freeze admin config changes pending review.
RecoveryVerify segment deletion; trace copies in replays, mirrors, DLQs and analytics; apply the data-protection procedure.
PreventionConfig changes through reviewed code, ACL on alter-config, field-level redaction or encryption before append, retention alerts.
Guarantee that remainsAuthorized configuration drives retention; replay never reaches past declared retention.
TradeoffCrypto-shredding and redaction add key management and make debugging harder; short retention narrows replay.
InvariantI11, I9, I1.
F32: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.
F33, F34

Regional Failures and Asynchronous Mirrors

Two regions give you two failure domains and a policy question about what may cross between them. Both incidents are about what a regional failover can and cannot preserve.

F33 — A region fails while residency policy forbids mirroring

HarborCart runs two independent messaging fabrics, EU and US. Topic orders.eu carries European customers' data and is tagged so it is never copied across the border. At 14:02 the EU fabric loses most of its brokers because of a provider incident. US operations ask whether they can simply route EU checkouts to the US fabric until it recovers.

Swipe to pan
Independent regional fabrics and residency-constrained failoverRegions are separate logs, joined only where policy allowsResidency-tagged topics never mirror cross-border; non-resident topics may, asynchronouslyEU regionProducers& consumersIndependent log fabricorders.euresident · stays herecatalog.updatesnon-residentbrokers · controller · quotasUS regionProducers& consumersIndependent log fabricorders.usresident · stays herecatalog.updates (replica)non-residentbrokers · controller · quotasasync mirror — non-resident topics onlyIf the EU region failsorders.eu does not fail over to the US: that wouldbreak residency. Consumers wait or degrade, and thebusiness decides what "degraded" means up front.Non-resident topics can fail overThe replica may lag the origin, so the unmirrored tailcan be lost and offsets differ across fabrics. Name theRPO and RTO you accept; never claim zero.
Each region runs its own fabric; residency-tagged topics stay inside, and only non-resident topics are mirrored asynchronously.

The honest answer depends on policy, not on engineering convenience. Routing EU events into the US would violate the residency rule, so it is off the table. The EU checkout path therefore has a defined degraded mode: accept orders into each region's local database and outbox, hold the events in the outbox until the EU fabric returns, and show customers an honest 'confirmation will follow' state. Nothing is lost provided the outbox survives, and the relay drains the backlog afterward.

Non-resident topics (orders.global, say) can be mirrored asynchronously and fail over, with a cost (see F34). Residency-tagged topics trade that failover for compliance. The business should know in advance which side of that line each topic is on, rather than discover it during an outage.

FacetF33 at a glance
TriggerMajority of EU brokers unavailable; EU topics are residency-restricted.
Timeline1) EU brokers fail at 14:02. 2) EU publishers see timeouts; outboxes begin accumulating rows. 3) Ops confirm that cross-border rerouting is not permitted. 4) Degraded checkout messaging is enabled for EU. 5) The EU fabric returns and relays drain the backlog.
ImpactEU confirmations, notifications and analytics are delayed; US operations are unaffected.
Root causeA regional dependency with no permitted failover target, by design.
DetectionRegional broker health, outbox row age per region, publish error rate by region.
ContainmentEnable degraded mode: keep accepting orders into the local database, show delayed-confirmation messaging, protect the outbox from growth limits.
RecoveryLet the relays drain at a controlled rate; verify consumers dedupe; confirm no cross-border data movement occurred.
PreventionPer-region capacity and redundancy across zones, documented topic residency classes, game-day rehearsal of degraded mode.
Guarantee that remainsResident data never leaves its region; committed orders are preserved by the local database and outbox.
TradeoffCompliance reduces availability: during a regional incident, EU workflows slow down rather than fail over.
InvariantI8, I10, I11.
F33: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F34 — Async mirror falls behind, then is promoted with a missing suffix

For a non-resident topic, HarborCart replicates US to a standby cluster asynchronously. During a busy hour the mirror link slows, and the standby trails by 12 s. At 20,000 events/s that gap is 240,000 events, about 234 MiB at 1 KiB each (illustrative). The primary region then fails and the team promotes the standby.

The standby is a consistent prefix of the primary's log, but it is missing the last twelve seconds. Producers acknowledged those events under the primary's replication policy; the standby never saw them. After promotion, consumers either re-read events they had already handled (their offsets from the old cluster do not map onto the new log) or silently skip the gap, depending on how offsets were translated.

This is the price of asynchronous cross-region copying: recovery point is greater than zero. The decision to promote must weigh that loss against continued unavailability, and the missing events can often be rebuilt from upstream sources of truth. For orders, the order database and outbox still hold them, and the relay can republish with the same event ids so inbox dedupe absorbs the overlap. If the old primary comes back holding that missing suffix, it cannot simply resume as primary: its log has diverged from the promoted cluster's, so it must be rebuilt or reconciled before any failback.

FacetF34 at a glance
TriggerPrimary region lost while the mirror is 12 s behind at 20,000 events/s.
Timeline1) Mirror link slows; lag grows to 12 s. 2) Primary region fails. 3) Standby promoted. 4) Consumers resume against a log missing 240,000 recent events. 5) Outbox republish fills the gap.
ImpactMissing suffix of acknowledged events; offset translation ambiguity; duplicates after republish.
Root causeAsynchronous replication across regions guarantees a bounded-lag copy, not zero data loss.
DetectionMirror lag in seconds and events; alerts before the lag exceeds the declared recovery point; consumer offset translation checks.
ContainmentDecide on promotion with the recovery point known; freeze consumers until offsets are mapped.
RecoveryRepublish from the order database or outbox with stable ids; reconcile counts per partition; dedupe with inbox tables.
PreventionAlert on mirror lag, document the recovery-point objective, keep an authoritative source for each topic, test the failover.
Guarantee that remainsAfter promotion the standby log is a consistent prefix of the old primary's partitions; events republished with the same ids are deduplicated.
TradeoffSynchronous cross-region replication could close the gap, at the cost of write latency on every publish.
InvariantI8, I10.
F34: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.
F29, F37

Observability Failures

You can only run what you can see accurately. One incident misreads a metric because of clocks, and the other loses all visibility because monitoring shares the broken cluster.

F29 — Clock skew misreports lag

A team builds a lag dashboard from event time: now minus the eventTime field the producer wrote. One producer fleet's clocks are 180 s behind. The dashboard shows email-notifier three minutes late, even though it consumed the event milliseconds after it was appended.

The on-call engineer sees a red graph and scales the consumer group from 12 to 16. Partitions are 24, so scaling is possible but not needed, and a rebalance is triggered for nothing. In the opposite direction, a fleet whose clocks run fast makes real lag look smaller or even negative, hiding a true delay.

Use two measures and understand each. Offset lag (the log end offset minus the committed offset) uses no clock. Oldest message age uses the broker's append timestamp, which relies on the broker's clock instead of the many producer clocks. Event-time lag has value for business questions such as 'how stale is this report,' and it must carry an error bar from clock synchronization.

FacetF29 at a glance
TriggerProducer clocks 180 s behind real time; lag is computed from event time.
Timeline1) Dashboard subtracts eventTime from now. 2) It reports 180 s of lag that does not exist. 3) On-call scales the group. 4) A rebalance occurs with no benefit.
ImpactWasted action and a rebalance; real lag could be hidden by fast clocks.
Root causeA client-supplied timestamp was used as a measurement of system delay.
DetectionCompare offset lag, broker-append age and event-time lag; NTP offset monitoring.
ContainmentTrust offset lag first; disable the event-time alert.
RecoveryFix the clock synchronization; redefine the metrics.
PreventionAlert on offset lag and append-time age; keep clocks synchronized; annotate event-time metrics with skew.
Guarantee that remainsOffset lag is clock-independent and exact for log position.
TradeoffAppend-time age measures time in the log, not time since the business event occurred.
InvariantI12.
F29: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.

F37 — The metrics pipeline depends on the broken cluster

HarborCart ships broker, consumer and lag metrics through the same message platform that it monitors. When a controller problem makes orders unavailable, the telemetry topic stalls too. Dashboards flatline at 09:14, which looks like 'quiet', and the alert that would have fired never arrives because it depends on the same path.

A monitoring system that shares fate with the monitored system fails exactly when it is needed. The pattern appears in many incidents: the status page needs the database that is down, the on-call tool needs the VPN that is down. Here the symptom is subtle because a missing series reads as zero lag, and the graph looks healthy.

The remedy is independence. Brokers and clients should expose metrics through a path that doesn't traverse the cluster (scraped over HTTP by a separate monitoring stack), alerts should treat missing data as a signal (a dead-man's switch that fires when no heartbeat arrives), and there should be a synthetic producer-consumer probe running end to end with its own alert route.

FacetF37 at a glance
TriggerTelemetry published via the same cluster that is failing.
Timeline1) Controller trouble degrades publish and fetch. 2) Telemetry topic stops flowing. 3) Dashboards go flat at 09:14. 4) Support tickets, not alerts, reveal the incident.
ImpactDetection delayed until customers report; longer incident duration.
Root causeShared fate between monitoring and the monitored system.
DetectionHeartbeat absence, synthetic probe failure, out-of-band checks.
ContainmentSwitch to out-of-band metrics and logs; page by the dead-man's-switch route.
RecoveryBackfill missing series where possible; mark the gap on dashboards.
PreventionSeparate metrics path, absent-data alerts, synthetic end-to-end probe, runbooks that don't need the cluster.
Guarantee that remainsLag, age, under-replication and dead-letter counts stay visible independently of data-plane health.
TradeoffA second monitoring path costs money and upkeep; it is the price of seeing outages.
InvariantI12.
F37: hypothetical HarborCart scenario; every number is a teaching input, not a measurement.
Operations

How to Run This Platform: Signals, Game Days and Rules

The incidents reduce to a short list of signals, a few drills and some standing rules. Having them before the first failure turns most of the forty into short events.

Signals worth paging on, and signals worth a ticket

SignalWhat it tells youTypical responseIncidents
Publish error rate and unknown-outcome countProducers cannot tell whether events landed (F03)Page when sustained; check broker health and idempotent publishingF03, F04, F09
Under-replicated partitions and ISR sizeReplication is behind or a broker is gonePage; with ISR at one writes are at riskF05, F06, F07, F08
Consumer lag in records and oldest-record age, per group and partitionBacklog and its business impactPage on age relative to retention; ticket on slow growthF21, F22, F23, F36, F39, F40
Retries per event, DLQ depth and DLQ agePoison or systemic downstream failurePage on rate change; ticket on ageF16, F17, F18, F19
Rebalance count and duplicate-id hits in inbox tablesOwnership churn and redeliveryTicket; page if sustained during deploysF11, F14, F15
Lease expirations versus job durationLeases too shortTicket and tuneF13
Outbox oldest unpublished rowRelay stalled or broker downPage past the freshness objectiveF27, F28, F33
Disk and quota headroom per broker and tenantBurst or abuse approaching limitsPage before fullF24, F31
Controller, metadata and connection countsControl-plane stressPage; apply client backoffF25, F37
Config and ACL change audit; schema registry changesUnreviewed changesReviewF20, F30, F32, F38
Mirror lag in secondsRecovery point is growingPage above the objectiveF34
Heartbeat and synthetic probeMonitoring is alive and end-to-end worksPage when absentF37
Operational signals mapped to incidents. Thresholds are team decisions, not defaults.

Set alerts relative to consequences. Lag of 3,000,000 records may be a minute at peak or an hour at night; oldest-record age and the ratio of lag to retention say far more. A consumer-lag figure alone cannot tell you the difference between a healthy burst and a stuck group.

Game days worth running

  • Kill one broker during load and confirm writes continue with two in-sync replicas, then kill a second and confirm writes are refused rather than lost (F05 to F07).
  • Crash a consumer after the database commit and before the offset commit; confirm the replay is absorbed by the inbox table (F11).
  • Run a rolling deploy of consumers and count duplicates per rebalance (F14, F15).
  • Publish a poison event and verify it reaches the DLQ after five attempts without stalling the partition (F16, F17).
  • Pause a group for longer than half of retention and check that the alert fires before data expires (F23, F36).
  • Fail the EU fabric and rehearse the degraded mode and outbox drain (F33).
  • Fail the cluster and confirm that the dead-man's-switch alert still arrives (F37).
  • Acknowledging or committing before the business effect is durable (F12).
  • Treating a DLQ as proof that the effect did not happen (F19).
  • Trusting a tenant id in the payload instead of the verified principal (F30).
  • Using event-time lag alone to decide on scaling (F29).
  • Scaling consumers past the partition count and expecting more throughput (F40).
  • Relaxing schema compatibility to ship faster (F20, F38).
  • Monitoring a cluster through the same cluster (F37).

Verification notes

Platform defaults quoted here were checked against the Apache Kafka source (producer delivery timeout of 120 s, at most five in-flight requests with idempotence, consumer session timeout of 45 s, offsets retention of 7 days, tombstone retention of 24 hours, log retention of 7 days by default, unclean leader election off by default). Consumer acknowledgment, automatic requeue on channel close and the delivery acknowledgement timeout come from the RabbitMQ documentation; the SQS visibility timeout, five-minute FIFO deduplication interval and dead-letter expiry rule come from the Amazon SQS developer guide. Behavior and defaults change between versions, and managed-service limits vary, so read the current documentation of whatever you run. Every HarborCart figure is illustrative.

Key takeaways
  1. 01Every incident sits on a boundary between two delivery states; name both states and the failure usually becomes clear.
  2. 02Duplicates are normal under at-least-once delivery; stable event ids and inbox tables make them harmless.
  3. 03Committed data survives only the failures the replication policy covers, and write availability is a separate promise.
  4. 04A DLQ parks events; it does not prove the effect did not happen, and redrive can reorder.
  5. 05Parallelism is bounded by partitions, ordering by the key, replay by retention.
  6. 06Monitoring has to survive the failure of the thing it watches.