Skip to content

A broker outage exhausts every pending row's attempt budget; when Kafka returns the relay publishes nothing #32

Description

@AlexeyShalaev

Measured on 0.2.0 with aiokafka 0.14.0 and a Kafka container paused mid-run, so the broker neither answers nor refuses. Twenty pending outbox rows, OmniBoxDomainService(max_attempts=6) (the default), KafkaEventPublisher(max_infra_retries=1), a relay loop calling publish_batch once per cycle:

--- Kafka paused; the relay keeps ticking every cycle ---
  cycle 1 at  40.1 s: {'pending/attempts=1': 20}
  cycle 2 at  80.2 s: {'pending/attempts=2': 20}
  cycle 3 at 120.3 s: {'pending/attempts=3': 20}
  cycle 4 at 160.5 s: {'pending/attempts=4': 20}
  cycle 5 at 200.6 s: {'pending/attempts=5': 20}
  cycle 6 at 240.7 s: {'failed/attempts=6': 20}
  cycle 7 at 240.7 s: {'failed/attempts=6': 20}

--- Kafka is back ---
  next relay cycle: {'failed/attempts=6': 20}; messages on the topic: 0
  after requeue_failed on every failed row and one more cycle: {'completed/attempts=0': 20}; messages on the topic: 20

Every cycle the broker is unreachable costs every pending row one attempt, and after max_attempts cycles the whole backlog is failed, which is terminal. When Kafka returns the relay publishes nothing, because fetch_and_lock_pending filters on attempts_made < max_attempts. The events are recovered only by someone running requeue_failed on each row. A relay ticking every second turns a four-minute broker outage into a backlog that never drains on its own, and the outbox's one promise, that nothing is lost, is kept only in the sense that the rows are still in the table.

The attempt budget is the right tool for a row that cannot be published: a payload the broker rejects, a topic that does not exist, a serialization error. It is the wrong tool for a broker that is not there, which is a property of the cycle, not of any row. Rule 13 says nothing classifies errors for the pipeline and that ErrorClassifier is used only inside the Kafka publisher's own infrastructure retry; once max_infra_retries is spent the exception reaches the pipeline as a counted failure like any other.

What I think it needs: infrastructure failures from the broker (connection, metadata, request timeout, the classes ErrorClassifier already knows) should reach the outbox pipeline as a non-counted failure with a next_retry_at backoff, the way handler_retry(count_as_attempt=False, next_retry_at=…) already lets a handler say "not my fault, try later". The publisher step is the natural place: it has the exception, it has the classifier, and the pipeline already supports the outcome. A counted failure stays reserved for errors that are about the event. With that, the table above ends with twenty pending rows that publish on the first cycle after Kafka returns, and requeue_failed goes back to being an operator's tool for genuinely poisoned rows. The docs (rule 13, the outbox guide, the runbook if there is one) follow.

Script: kafka_down_lab.py in https://github.com/bedrock-python/bedrock-python.github.io/tree/docs/production-python-series/docs/blog/lab/2026-09-07-when-kafka-is-down. It takes about five minutes because the paused cycles wait on aiokafka's request timeout per event.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions