Guarantees & Errors¶
This page is the contract. Everything Narad promises, the limits of those promises, and every status code you can see.
The delivery guarantee¶
At-least-once. Every message that gets a 202 will be delivered to a consumer, and will keep being redelivered until someone acks it (or retention expires). The flip side: duplicates happen — after consumer crashes, visibility timeouts, nacks, ambiguous produce retries, and node failures. Your consumer must be idempotent. This is not optional advice; it's the other half of the contract.
flowchart LR
A["202 Accepted"] --> B{delivered and acked?}
B -->|yes| C[settled — never seen again]
B -->|"consumer crashed / timed out"| D[redelivered — possibly a duplicate]
D --> B
B -->|"never acked until retention"| E[deleted by retention]
The durability contract — read before trusting Narad with anything important¶
- A
202 Acceptedmeans your message is fsynced to disk on the accepting node before the response is sent. A crash one millisecond later does not lose it. - On its final partition, a message is fsynced and read-back-verified before it becomes visible to consumers or is removed from the accepting node's log.
- Narad keeps exactly one copy of each partition (plus the transit copy above). There is no replication subsystem. If a node's disk is permanently destroyed, the messages on that disk's partitions are gone. Process crashes, restarts, and node reboots lose nothing — this has been chaos-tested extensively — but disk loss is unprotected by default. Run it on storage you trust (cloud persistent volumes, RAID), and for topics that need a second copy, create a replica child: replication, when you ask for it — an async full copy, deliberately placed on different nodes.
- Cluster metadata (topics, users, assignments) is Raft-replicated across all nodes and survives any minority of disks failing.
Ordering: not guaranteed¶
Narad is explicit about this where other brokers are shy: there is no delivery-order guarantee. In steady state, keyed messages stick to one partition and tend to arrive in produce order — but three deliberate mechanisms reorder, and your design must assume them:
- Redelivery: a crashed or slow consumer's message reappears after newer ones were consumed.
- Dead-owner skip: while a node is marked dead, keyed produces walk forward to a live partition — the key-to-partition mapping itself moves.
- Dispatch reroute: accepted messages destined for an unreachable owner are committed to a live sibling partition rather than delayed indefinitely.
Need a sequence? Carry it in the payload and order on your side. Need to collapse duplicates and reorders at once? Make handlers idempotent keyed on a payload ID — which the at-least-once contract requires anyway.
Availability: the deliberate trade¶
Ordering was not lost by accident — it was spent on availability. In CAP terms Narad's data plane is AP:
- Produce is available while any node lives. Any live node accepts a produce with a local fsync — no leader election, no quorum, no coordination on the hot path. Delivery is worked out afterwards and routed around dead machines.
- Consume is available for every partition whose owner is alive — and since new traffic reroutes to live owners, fresh messages stay consumable even mid-outage. Messages already stored on a dead node wait for it to return (their partition answers
503meanwhile). - The control plane is the one consistent piece: creating/altering topics and managing users go through Raft and need a quorum of nodes. Your data flows at one node; administration waits for a majority.
flowchart LR
subgraph ap["Data plane — available (AP)"]
PRO[produce: any live node]
CON[consume: live owners]
end
subgraph cp["Control plane — consistent (CP)"]
META["topics / users / membership<br/>need a Raft quorum"]
end
Timing¶
- Produce→consumable: typically single-digit milliseconds.
- Delay children: never early; usually within ~1s after the delay elapses; can be later under failures.
- Retention: messages are deleted at least
retention_msafter writing — deletion happens in coarse chunks, so data often lives somewhat longer, never shorter.
Status codes¶
| Code | Where | Meaning | What you should do |
|---|---|---|---|
200 |
consume, reads | Here's your data | Process it |
201 |
topic/user create | Created | — |
202 |
produce | Durably accepted — the delivery promise | Nothing. Never retry a 202 |
204 |
ack/extend/nack, delete, empty consume | Done / nothing available | Loop or move on |
400 |
anywhere | Malformed request, bad param, schema violation | Fix the request; don't retry as-is |
401 |
anywhere | Missing/wrong credentials | Fix auth |
403 |
anywhere | Authenticated but not allowed | You need a grant |
404 |
anywhere | Topic/user doesn't exist | Check the name |
409 |
create/attach/alter | Conflict: already exists, role conflict, retention-vs-delay violation | Read the error body |
410 |
ack/extend | Your lease lapsed; message was handed elsewhere | Stop working on it; expect a duplicate |
413 |
produce | Body over 1 MiB | Shrink the payload |
503 |
produce/consume/ack | Temporarily unavailable: partition owner down, acked-ahead full, quorum lost | Back off and retry |
Retry cheat sheet¶
202→ never retry.4xx→ never retry unchanged.503and timeouts → retry with backoff.- A timed-out produce is ambiguous: the message may have been accepted. Retrying may duplicate it — that's fine, because your consumer is idempotent. Right?
API stability¶
The /v1 surface is stable: routes, parameters, status codes, and JSON field names documented in this guide won't change or disappear within /v1. New optional fields and new endpoints may appear (your JSON parsing should ignore unknown fields — it already does, right?). Anything we ever need to break moves to a /v2 with both versions serving during a deprecation window. The node-to-node RPC protocol is versioned by the same rule: opcodes are append-only, and unknown ops get a clean 400 — which is what makes mixed-version rolling upgrades boring.