Skip to content

Before vs After

When a consumer commits its offset determines what happens on crash and restart. Commit before processing = risk losing messages. Commit after processing = risk duplicates. There is no free lunch — pick your poison and design around it.


The question#

A consumer reads a batch of 1000 events (offsets 0–999). When should it commit offset 999 — before processing the batch or after?


Commit before processing — At-Most-Once#

sequenceDiagram
    participant C as Consumer
    participant K as Kafka

    C->>K: read batch offsets 0-999
    C->>K: commit offset 999 ← committed BEFORE processing
    C->>C: start processing...
    C->>C: crashes at offset 500
    C->>K: restart → "where was I?"
    K->>C: offset 999 (last committed)
    C->>K: read from offset 1000
    Note over C: offsets 500-999 never processed, never redelivered — LOST

What happened: the consumer told Kafka "I'm done up to 999" before actually being done. On restart, Kafka has no idea the crash happened — it thinks 0–999 were processed successfully.

Messages 500–999 are lost forever.

This is at-most-once — each message is processed zero or one times. Never duplicated, but can be lost.

When to accept this: metrics, analytics events, logs — where losing one click event out of a million is acceptable and duplicates would corrupt aggregations.


Commit after processing — At-Least-Once#

sequenceDiagram
    participant C as Consumer
    participant K as Kafka

    C->>K: read batch offsets 0-999
    C->>C: start processing...
    C->>C: crashes at offset 500 ← never committed
    C->>K: restart → "where was I?"
    K->>C: offset 0 (last committed before this batch)
    C->>K: read from offset 0
    Note over C: offsets 0-499 processed TWICE — duplicates
    C->>C: processes 0-999 again
    C->>K: commit offset 999

What happened: the consumer crashed before committing. On restart, Kafka correctly redelivers the entire batch. Offsets 0–499 were already processed in the previous run — they get processed again.

Messages 0–499 are duplicated.

This is at-least-once — each message is processed one or more times. Never lost, but can be duplicated.

When to accept this: almost everywhere. It's the Kafka default and the industry standard. The duplicate problem is handled at the consumer level.


The fix for duplicates — idempotent consumer#

Commit after processing (at-least-once) + make the consumer idempotent = effectively exactly-once behaviour, without the cost of true exactly-once infrastructure.

Example — Billing Service counting clicks:

First processing of offset 500 (click_id: abc123):
→ Check DB: has click_id abc123 been counted?
→ No → insert { click_id: abc123, advertiser: X, count: 1 }

Second processing of offset 500 (duplicate from retry):
→ Check DB: has click_id abc123 been counted?
→ Yes → skip, do nothing, ACK

Result: advertiser X charged exactly once

Or simpler — use a DB unique constraint:

INSERT INTO click_counts (click_id, advertiser_id, amount)
VALUES ('abc123', 'X', 0.05)
ON CONFLICT (click_id) DO NOTHING

Duplicate delivery hits the unique constraint and is silently skipped. No double charges.


Summary#

When to commit Delivery guarantee Risk Fix
Before processing At-most-once Message loss Accept it for non-critical events
After processing At-least-once Duplicates Idempotent consumer

The fix for duplicates lives in the consumer, not in Kafka. Kafka delivers at-least-once by default. Your consumer is responsible for handling duplicates safely. This is a consumer-side design decision, not a Kafka configuration.

Interview framing: "I'd commit offsets after processing — at-least-once delivery. To handle the duplicate risk, I'd make the consumer idempotent using a unique constraint on the event ID at the DB level. This gives effectively exactly-once semantics without the cost and complexity of Kafka's transactional API."

Never commit before processing unless you've explicitly accepted message loss as a trade-off and documented why. It's easy to do accidentally and very hard to debug when events silently disappear.