Stream destinations
The destination set#
A stream fans out to one or more destinations. Each destination drains independently, so a slow or broken sink does not stop the others.
| Group | Engines |
|---|---|
| Message broker | kafka |
| Relational | postgres, mysql |
| Object / file | s3, azure_blob, gcs, ftp, ftps, local |
Connection fields for each engine — including how host, database and the
credential fields are repurposed for object stores and Kafka — are documented in
Connectors.
Kafka#
| Setting | Meaning |
|---|---|
| Topic | The target topic. This is the destination — there is no per-record routing. |
| Partition-key field | Optional. The record field whose value becomes the message key. |
Behaviour:
- Ordering is per-partition only. With a partition-key field set, all records sharing a key land on one partition and keep their relative order. Without a key, records round-robin across partitions and no ordering is preserved at all.
- Produced with
acks=all, one record per send — the broker acknowledges every record before it is considered delivered. - Each message carries an
abrq_idempotency_keyheader so consumers can deduplicate a redelivered record. - Kafka ignores truncate instructions and takes no part in schema evolution. It is a sink and nothing more.
Object and file destinations#
Applies to s3, azure_blob, gcs, ftp, ftps and local.
| Setting | Values |
|---|---|
| Format | jsonl, parquet, json, csv |
| Compression (text formats) | none, gzip |
| Compression (parquet) | none, snappy, gzip, zstd |
| Date partitioning | Optional dt=YYYY-MM-DD/ prefix |
| Manifest sidecar | On by default for ftp / ftps |
One object per batch. The object name is
part-<idempotency-prefix>.<ext> — derived from the batch's idempotency key,
with no timestamp component. That is deliberate: replaying a batch writes
the identical object name, so a replay overwrites rather than duplicating.
exports/orders/dt=2026-07-30/part-8f2c41a9.jsonl.gz
exports/orders/dt=2026-07-30/part-8f2c41a9.jsonl.gz.manifest.json
- Date partitioning, when enabled, inserts a
dt=YYYY-MM-DD/directory keyed off the time the batch was staged, not the time it was written. A retried batch therefore lands in the same partition as its first attempt. - The
.manifest.jsonsidecar carries the object path, row count, byte count and asha256of the object. It is enabled by default for FTP and FTPS, where there is no server-side integrity check to lean on. - FTP and FTPS write to a temporary name and then rename. A consumer polling the directory never observes a partially written object.
Relational destinations#
postgres and mysql only.
- Each batch is written as a batched
INSERTinside one transaction. The batch lands completely or not at all. - Relational destinations are the only stream destinations that participate in schema evolution — see Schema drift policies.
Delivery semantics#
Note. Stream delivery is at-least-once with idempotency keys. Exactly-once is not claimed and not implemented. Design consumers to deduplicate on the idempotency key.
How that guarantee is achieved:
- Records are staged into a spool inside the source transaction. The source cursor cannot advance without the corresponding records being staged, so nothing is read-and-lost.
- Each destination keeps its own high-water mark, and that mark only advances past a batch the destination durably accepted.
- Head-of-line consequence: a destination that cannot accept a batch does not skip it. Its high-water mark stays put and its backlog grows until the batch succeeds or is dead-lettered. The other destinations are unaffected.
Retries#
| Setting | Default |
|---|---|
| Attempts | 3 |
| Base delay | 2 s |
| Backoff | Exponential with full jitter |
| Maximum delay | 300 s |
| Per-write timeout | 60 s |
Full jitter means the actual delay is drawn uniformly between zero and the computed exponential ceiling, so a fleet of retrying workers spreads out instead of synchronising into one simultaneous retry surge.
Circuit breaker#
| Setting | Default |
|---|---|
| Consecutive failures to open | 5 |
| Cooldown while open | 300 s |
| Half-open trials | 1 |
While the circuit is open the destination is skipped entirely — no attempts, no timeouts, no queue pressure on the worker. After the cooldown a single trial batch is allowed through; success closes the circuit, failure re-opens it for another cooldown.
An operator can force the issue with the Reset breaker action, which closes the circuit immediately without waiting out the cooldown. Use it after fixing the underlying problem — resetting into a still-broken sink just re-opens it.
The counter abrq_stream_fanout_circuit_opened_total{destination_kind} records
every trip.
Dead letters#
A batch that exhausts its retry budget is dead-lettered for that destination only. Every dead letter carries a reason class:
| Reason class | Meaning |
|---|---|
validation |
The records were rejected as malformed for this destination |
timeout |
The write exceeded the per-write timeout |
source |
The failure originated on the source side of the drain |
destination |
The sink rejected or was unreachable |
other |
Anything not classified above |
Two operator actions exist:
- Replay — re-drive the dead-lettered batch to the one destination that dead-lettered it. Replay is targeted; it never re-sends to healthy sinks.
- Discard — drop the batch permanently. This is data loss and is recorded as such.
Spool retention#
The spool is garbage-collected by high-water mark: batches at or below the minimum high-water mark across all enabled destinations are deleted.
Warning. Only enabled destinations count. Disabling a destination removes it from the minimum, which lets the spool GC advance past batches it never received — re-enabling it later will not backfill them. A destination you intend to resume should be left enabled and allowed to fall behind, not disabled.
Metrics#
| Metric | Labels |
|---|---|
abrq_stream_fanout_deliveries_total |
destination_kind, outcome |
abrq_stream_fanout_dead_letters_total |
destination_kind |
abrq_stream_fanout_circuit_opened_total |
destination_kind |
abrq_stream_runs_total |
status |
abrq_stream_rows_total |
none |
See Metrics.