Queuing Behaviour of Otel Collector

OpenTelemetry Collector — sending_queue behaviour during a backend outage

OpenTelemetry Collector — sending_queue behaviour during a backend outage

Single pipeline, in-memory queue only — how queue_size (250 vs 1000) and block_on_overflow (true / false) change what happens when the aggregator is unreachable

How the sending_queue works
Every exporter has an in-memory sending_queue that buffers outgoing batches while the exporter is busy or the backend is slow.

When the backend is healthy, batches flow through quickly and the queue stays near-empty.

When the backend is down, the exporter retries with exponential backoff (default max_elapsed_time 5 min) and the queue fills at the incoming rate. Two knobs govern what happens next:

queue_size — capacity in batches (default 1000).
block_on_overflow — when full, drop (false, default) or block the caller (true).
Telemetry Sources Applications Pods Host Metrics Logs Traces OTLP (gRPC/HTTP) Son Testing K8s Cluster (Source — Telemetry Producers) OpenTelemetry Collector (single pipeline · in-memory sending_queue only) INGRESS PIPELINE (Receivers) otlp receiver Receives Traces, Metrics & Logs Processors → Exporter PROCESSING Batching · Resource Detection · Transformation (memory_limiter can refuse here if downstream stalls — see note →) OTLP EXPORTER (producer-consumer: ingest fills the queue; workers drain it to the backend) otlp exporter sending_queue (memory) ← tail (enqueue) head (drain →) queue_size · block_on_overflow govern capacity & overflow workers retry_on_failure exponential backoff · max_elapsed_time 5m otelcol_exporter_queue_size / _capacity otelcol_exporter_enqueue_failed_* Observability Dev K8s Cluster (Observability Platform) Otel Aggregator Service (OTLP Receiver + Processing) OTLP Receiver (ingest from source collector) — unreachable during outage Processing Batching · Resource Detection Transformation · Routing Mimir (Metrics) Loki (Logs) Tempo (Traces) OTLP export (gRPC/HTTP) Backend outage retries fail, queue fills Important Notes
  • No file_storage — the queue is RAM only; a Collector restart loses everything still in it.
  • queue_size is measured in batches (default sizer); pick it from peak throughput × outage-budget.
  • Retries stop after max_elapsed_time (default 5 min) — the oldest items are then dropped even if the queue has room.
  • block_on_overflow = true trades drops for backpressure: receivers slow, clients retry, upstream may buffer or drop.
  • Watch otelcol_exporter_queue_size vs _capacity and enqueue_failed_*.
1 NORMAL OPERATION (backend healthy)
  • Batches are enqueued and workers drain them immediately.
  • Queue depth stays near zero — the buffer is only ever a few items deep.
  • queue_size and block_on_overflow have no observable effect.
sending_queue depth ~1 / 1000 Ingest sending_queue near-empty Backend ✓
Result: Steady state, no drops, low memory.
2 OUTAGE STARTS — QUEUE FILLS (backend unreachable, retries in flight)
  • Exports fail; the exporter retries with exponential backoff.
  • New batches keep arriving and are enqueued instead of sent — depth climbs at the incoming rate.
  • Time-to-full depends on queue_size:
queue_size = 250 ~25 s @ 10 batches/s queue_size = 1000 ~100 s @ 10 batches/s Same ingest rate → larger queue buys more time before overflow.
Result: No loss yet — clock is ticking against queue capacity.
3a OVERFLOW · block_on_overflow = false (default — queue is full, new data is dropped)
  • Enqueue returns failure immediately; the incoming batch is dropped at the exporter and never reaches retry logic.
  • Logs: \”Dropping data because sending_queue is full\”; metric otelcol_exporter_enqueue_failed_* increments.
  • Upstream (receiver, client) is unaffected — ingest continues at full rate, but the collector silently sheds load.
Ingest sending_queue FULL (250 or 1000) dropped enqueue_failed++ Backend ✕
Result: Data loss. Collector stays responsive; sources keep streaming.
3b OVERFLOW · block_on_overflow = true (queue is full, caller is blocked until space frees)
  • The call into the exporter blocks; if capacity frees before the caller’s timeout, the item is still enqueued.
  • Backpressure propagates: processors pause → receiver slows → OTLP clients see timeouts/errors and retry or buffer themselves.
  • Trades exporter-side drops for upstream-side drops or latency. Good when upstream can hold data (SDK retry) or slow down.
Ingest backpressure sending_queue FULL — caller waits Backend ✕
Result: No exporter drops, but upstream slows or refuses — loss moves out of the collector.
Behaviour matrix — what the collector does when the aggregator is downAssumes steady ingest of 10 batches/s and default retry_on_failure.max_elapsed_time = 5m. Times are illustrative — real throughput depends on batch size and processor load.
queue_sizeblock_on_overflowtime to fill
(@ 10 batches/s)
on overfloweffect upstream
(receiver / client)
memory costtrade-off · when to choose
250false~25 seconds (250 / 10)Exporter drops incoming batches; enqueue_failed_* increments.None — ingest continues at full rate; the loss is invisible to producers.Low (~250 batches in RAM)Cheap, safe from OOM, but tiny outage budget. Fine when SDK retry is strong or some data loss is acceptable.
250true~25 seconds — then stallsCaller blocks waiting for space; no exporter-side drops.Backpressure hits processors & receivers quickly (~25 s in). OTLP clients see timeouts, will retry or drop.Low (~250 batches in RAM)Push loss upstream fast. Best when producers can buffer or are explicitly designed to slow down.
1000false (default)~100 seconds (1000 / 10)Same as row 1 once full — drops at the exporter, enqueue_failed_* rises.None — ingest unaffected; larger buffer just delays when drops start.~4× row 1 (~1000 batches)Common default. Rides short blips; outages > ~100 s (or > 5 min retry window) still lose oldest data.
1000true~100 seconds — then stallsCaller blocks; no exporter-side drops until the retry window lapses on old items.Backpressure arrives later (~100 s) but just as hard — and lasts until the backend recovers.~4× row 2 (~1000 batches)Maximum in-memory durability without a disk queue. Risk: OOM if outage outlasts memory headroom.
OpenTelemetry Collector — file_storage persistent queue behaviour during a backend outage

OpenTelemetry Collector — file_storage persistent queue during a backend outage

Two-tier queue: in-memory sending_queue in front of a persistent file_storage extension on a PVC — how queue_size (15 000 vs 25 000) and block_on_overflow (false / true) change what happens when the aggregator is unreachable

How the two-tier queue works
The exporter still has a small in-memory sending_queue for worker handoff; behind it, the file_storage extension persists every enqueued batch to a PVC before the producer is acknowledged.

Write path: producer → WAL on disk → ack. Read path: workers consume from disk, attempt export, ack + delete on success, retry on failure.

Two knobs set behaviour during a backend outage:

queue_size — capacity in batches (here 15 000 or 25 000). At 10 batches/s that’s ~25 min or ~42 min of buffer.
block_on_overflow — when disk queue is full, drop (false) or block caller (true).

Key benefit vs in-memory: on Collector restart the queue persists. Work already on disk resumes draining when the backend recovers.
Telemetry Sources Applications Pods Host Metrics Logs Traces OTLP (gRPC/HTTP) Son Testing K8s Cluster (Source — Telemetry Producers) OpenTelemetry Collector (single pipeline · sending_queue + file_storage persistent extension) INGRESS PIPELINE (Receivers) otlp receiver Receives Traces, Metrics & Logs Processors → Exporter PROCESSING Batching · Resource Detection · Transformation OTLP EXPORTER · two-tier queue (in-memory queue hands off to file_storage; workers drain disk to backend) otlp exporter sending_queue (memory · handoff) file_storage (PVC) tail head queue_size: 15k / 25k persists on restart Persistent Volume Claim (/var/lib /otelcol) ~750 MB / 15k ~1.25 GB / 25k workers drain retry_on_failure exp. backoff · max_elapsed_time = ∞ otelcol_exporter_queue_size / _capacity otelcol_exporter_enqueue_failed_* Observability Dev K8s Cluster (Observability Platform) Otel Aggregator Service (OTLP Receiver + Processing) OTLP Receiver (ingest from source collector) — unreachable during outage Processing Batching · Resource Detection Transformation · Routing Mimir (Metrics) Loki (Logs) Tempo (Traces) OTLP export (gRPC/HTTP) Backend outage retries fail, file queue fills Important Notes
  • file_storage is a Collector extension; attach it to the exporter’s sending_queue.storage.
  • Queue survives restarts & crashes. On startup, workers resume from where they left off.
  • Every enqueue is a disk write — expect IOPS & fsync cost. Use a fast PVC class; avoid network storage with unpredictable latency.
  • Size the PVC 1.5–2× the batch-count budget for compaction headroom. Monitor kubelet_volume_stats_used_bytes.
  • If PVC fills, enqueue fails the same way queue_size limits do — block_on_overflow still decides drop vs block.
  • Single-writer by design — bind to a StatefulSet, not a Deployment; one Pod per PVC.
  • Backpressure still possible at the memory tier in front of it; the memory queue is small and just a handoff buffer.
1 NORMAL OPERATION (backend healthy)
  • Batches are written to the WAL, then drained immediately by workers; the PVC holds only a small working set.
  • Disk queue depth hovers near zero. Write amplification is roughly 1× enqueue-write + 1× delete-marker per batch.
  • queue_size & block_on_overflow have no observable effect.
file_storage depth ~10 / 25 000 Ingest sending_queue handoff file_storage near-empty Backend ✓
Result: Steady state. Disk writes are absorbed; no drops; tiny PVC footprint.
2 queue_size = 15 000 · block_on_overflow = false buffers ~25 min of outage, then drops
  • Backend unreachable; retries fail; every batch persists to disk. Queue climbs at 10 batches/s — full in ~1 500 s (~25 min).
  • After overflow, enqueue returns failure and newest batches are dropped at the exporter; enqueue_failed_* climbs. Already-queued data still waits on disk.
  • PVC footprint ≈ 750 MB (15 000 × ~50 KB).
file_storage depth (queue_size = 15 000) ~25 min to full @ 10/s Ingest sending_queue handoff file_storage FULL (15 000) dropped enqueue_failed++ Backend ✕
Result: Newest telemetry lost after 25 min; older queued data survives & will flush when backend returns.
3 queue_size = 15 000 · block_on_overflow = true buffers ~25 min, then blocks the caller
  • Same fill behaviour — full in ~25 min at 10 batches/s.
  • Once full, the exporter blocks until the backend drains an item from disk. Backpressure propagates: processors pause → receiver slows → OTLP clients see timeouts and retry or buffer themselves.
  • Good when producers can hold data; risky when upstream has no buffer of its own.
file_storage depth (queue_size = 15 000) ~25 min — then stalls Ingest backpressure sending_queue caller waits file_storage FULL — blocked Backend ✕
Result: No exporter drops; loss moves upstream to OTLP clients. PVC still ~750 MB.
4 queue_size = 25 000 · block_on_overflow = false buffers ~42 min of outage, then drops
  • Same mechanics as scenario 2 — bigger headroom. Queue fills in ~2 500 s (~42 min) at 10 batches/s.
  • After overflow, new batches are dropped at the exporter; older batches on disk continue waiting and will flush when backend returns.
  • PVC footprint ≈ 1.25 GB (25 000 × ~50 KB) — plus compaction headroom.
file_storage depth (queue_size = 25 000) ~42 min to full @ 10/s Ingest sending_queue handoff file_storage FULL (25 000) dropped enqueue_failed++ Backend ✕
Result: Bigger outage budget (42 min) for the cost of 1.25 GB disk; past that, drops resume.
5 queue_size = 25 000 · block_on_overflow = true buffers ~42 min, then blocks the caller
  • Same fill curve — full in ~42 min. This is the strongest durability configuration without sharding.
  • Once full, the exporter blocks; upstream sees timeouts. If outage outlasts ~42 min and clients can’t hold data, loss happens at producers.
  • Risk: PVC must comfortably hold > 1.25 GB — watch for compaction and fsync pressure.
file_storage depth (queue_size = 25 000) ~42 min — then stalls Ingest backpressure sending_queue caller waits file_storage FULL — blocked Backend ✕
Result: Maximum durability; collector stays bounded; loss pushed to producers after ~42 min.
6 RESTART DURING OUTAGE — QUEUE SURVIVES (the core reason to use file_storage)
  • Collector Pod crashes / is rolled / is OOM-killed mid-outage. In-memory queues would lose everything buffered.
  • With file_storage, the PVC is re-mounted on the new Pod. Exporter reads WAL head offset and resumes draining the same batches.
  • Only loss: anything in the small memory queue at the moment of crash (handoff window).
file_storage depth — before & after restart before 12 000 batches on disk Pod restart · PVC re-attached after ~12 000 batches preserved new Pod file_storage (PVC) workers resume Backend (when up)
Result: Near-zero loss across restarts. The defining advantage over the in-memory queue.
Behaviour matrix — in-memory only vs file_storage persistent queue, during a backend outage Assumes steady ingest of 10 batches/s, ~50 KB per batch, default retry_on_failure. Times and disk sizes are illustrative — real throughput depends on batch size and processor load.
queue_size block_on_overflow time to fill
(@ 10 batches/s)
on overflow effect upstream
(receiver / client)
survives restart? storage cost trade-off · when to choose
sending_queue in-memory only (no file_storage)
250 false ~25 seconds Exporter drops incoming batches; enqueue_failed_* increments. None — ingest continues at full rate; loss invisible to producers. no — RAM-only ~250 batches in RAM (~12 MB) Cheap, OOM-safe, tiny outage budget. OK when SDK retry is strong.
250 true ~25 s — then stalls Caller blocks; no exporter-side drops. Backpressure ~25 s in; OTLP clients see timeouts. no — RAM-only ~250 batches in RAM (~12 MB) Push loss upstream fast; use when producers can buffer.
1000 false (default) ~100 seconds Drops at exporter, enqueue_failed_* rises. None — ingest unaffected; larger buffer delays drops. no — RAM-only ~1000 batches in RAM (~50 MB) Common default. Rides short blips; > ~100 s still loses oldest.
1000 true ~100 s — then stalls Caller blocks; no exporter-side drops until retry window lapses. Backpressure arrives later but lasts until backend recovers. no — RAM-only ~1000 batches in RAM (~50 MB) Max in-memory durability; risk of OOM on long outages.
file_storage persistent queue on a PVC (in-memory tier in front)
15 000 false ~25 minutes (1 500 s) After 25 min, newest batches are dropped at the exporter; enqueue_failed_* rises. Already-persisted batches still wait on disk and flush when backend returns. None — ingest continues at full rate; loss invisible to producers. yes — persists & resumes ~750 MB PVC
(plus compaction headroom)
Good balance: ~25 min of buffer with bounded disk. Accepts tail-drops on long outages.
15 000 true ~25 min — then stalls Caller blocks when disk queue full; no exporter-side drops. Backpressure after ~25 min; OTLP clients retry or drop. Upstream becomes the bottleneck. yes — persists & resumes ~750 MB PVC Pushes loss upstream; best when producers can hold or slow down.
25 000 false ~42 minutes (2 500 s) Same mechanics as 15 000 but with ~17 more minutes of headroom before drops begin. None — ingest unaffected for the full ~42 min window. yes — persists & resumes ~1.25 GB PVC Larger outage budget at modest disk cost; still bounded drop behaviour.
25 000 true ~42 min — then stalls Caller blocks; no exporter-side drops until retry window lapses on oldest items. Backpressure after ~42 min; hardest guarantee without sharding. yes — persists & resumes ~1.25 GB PVC
(watch compaction / fsync)
Maximum single-node durability. Risk: slow PVC = slow ingest even when backend is healthy.
OpenTelemetry Collector with Failover Connector

OpenTelemetry Collector with Failover Connector, Sending_Queue and File_Storage

Resilient telemetry flow from Son Testing (Source) to Observability Dev (Aggregator + Backends)

How the Failover Connector Works
The failover connector is an internal component that acts as an exporter for the ingress pipeline and a receiver for the downstream pipelines. It maintains a prioritized list of destinations, routing telemetry to the first healthy pipeline. It periodically retries higher-priority pipelines to return healthy pipelines when they become healthy again.
Telemetry Sources Applications Pods Host Metrics Logs Traces OTLP (gRPC/HTTP) Son Testing K8s Cluster (Source — Telemetry Producers) OpenTelemetry Collector (Single Collector Process · Multiple Pipelines) INGRESS PIPELINE (Receivers) otlp receiver Receives Traces, Metrics & Logs Exports to Failover Connector FAILOVER CONNECTOR (Exporter for Ingress, Receiver for Downstream Pipelines) Priority Levels (Health-Based Routing) 1. primary (highest priority) 2. failover (lower priority) retry_interval 30s (configurable) Routes data to the first healthy pipeline Routes data to failover pipeline when primary pipeline is unhealthy PRIMARY PIPELINE (Fast Path · No Disk) otlp exporter sending_queue (memory) FAILOVER PIPELINE (Durable Buffer Path) otlp exporter sending_queue (memory) file_storage (Write-Ahead Log on Disk) Observability Dev K8s Cluster (Observability Platform) Otel Aggregator Service (OTLP Receiver + Processing) OTLP Receiver (Ingests from Primary and Failover pipelines) Processing Batching · Resource Detection Transformation · Routing Mimir (Metrics) Loki (Logs) Tempo (Traces) Primary OTLP (gRPC/HTTP) Live Data Both pipelines target the same Aggregator endpoint Failover OTLP (gRPC/HTTP) Buffered or Live Data Important Notes
  • The connector does not buffer or persist data — it only routes based on health.
  • Buffering and persistence are provided by the exporters (sending_queue and file_storage) in each pipeline.
  • Both pipelines send to the same aggregator endpoint.
  • After recovery, the connector routes new traffic to Primary while Failover drains backlog.
1 NORMAL OPERATION (Aggregator is Healthy)
  • Connector routes new incoming telemetry to the Primary Pipeline (highest priority, healthy).
  • Primary exporter sends data directly to the Otel Aggregator.
  • Failover pipeline is idle but ready.
Ingress Failover Connector → Primary to Failover (idle)
Result: Low latency, normal flow.
2 BACKEND OUTAGE (2 HOURS) (Aggregator Unavailable)
  • Primary exporter’s sending_queue starts buffering in memory. When it reaches limits or errors surface, the pipeline returns failure to the connector.
  • Connector marks Primary as unhealthy and switches traffic to the Failover pipeline.
  • Failover exporter’s sending_queue + file_storage buffer and persist telemetry safely for the duration of the outage.
! Aggregator Down Ingress Failover Connector Primary (unhealthy) Failover (active · buffering)
Result: No data loss (within capacity). Live traffic goes to failover; data is durable on disk.
3 BACKEND RECOVERY — BOTH SEND (Aggregator is Healthy Again)
  • Connector retries Primary on retry_interval (e.g., 30s).
  • When Primary is healthy again, connector routes new incoming telemetry back to Primary.
  • Failover pipeline continues sending its buffered backlog (from memory and disk) until fully drained.
  • Both pipelines may send simultaneously for a period:
  • – Primary sends fresh/new telemetry
  • – Failover sends older/buffered telemetry
Ingress Failover Connector → Primary (new data) → Failover (draining backlog)
Result: System returns to normal while ensuring no data loss during the outage window.
Key Components Failover Connector
Health-based routing between pipelines (no storage).
Primary Pipeline
Fast path to backend with in-memory queue only.
Failover Pipeline
Buffering path with in-memory queue + persistent file_storage.
Otel Aggregator
Central ingest and routing to Mimir (metrics), Loki (logs), Tempo (traces).
Arrow Legend Active live traffic Failover / buffered Idle / inactive
Failover Connector · Primary + Failover Queue Matrix

OpenTelemetry Collector · Failover Connector — Primary & Failover Queue Matrix

Primary pipeline ships live to the aggregator; when the failover connector trips, the failover pipeline’s two-tier queue (sending_queuefile_storage on a PVC) takes over. The matrix shows how a queue_size of 10 000 / 15 000 / 20 000 / 25 000 behaves during 1-hour and 2-hour aggregator outages, at both block_on_overflow settings.

Ingest rate 50 batches/s
Batch size ~50 KB/batch
Primary queue 1 000 batches · RAM
Failover storage file_storage on PVC
Drain rate (healthy) 50 batches/s · = ingest
retry_on_failure on · max_elapsed_time ∞

Primary + Failover queue behaviour — 1 h and 2 h aggregator outage

The Primary pipeline’s 1 000-batch in-memory queue covers only ~20 s before the failover connector routes traffic to the Failover pipeline. All long-outage behaviour (and all loss or backpressure) is governed by the Failover queue knobs below.

Failover queue configuration · 1-hour and 2-hour outage outcome Rows compare queue_size (10 k → 25 k) × block_on_overflow (false / true). Times to fill assume 50 batches/s sustained ingest; drain times assume backend returns at full health and runs at break-even (50 batches/s) while live ingest continues.
Failover
queue_size
block_on_overflow Time to fill
@ 50 / s
PVC
footprint
1-hour outage outcome
(180 000 batches arrive)
2-hour outage outcome
(360 000 batches arrive)
Drain time
after recovery
Recommended when…
primary_queue in-memory only · 1 000 batches · ~20 s of buffer
1 000 false (default) ~20 s RAM
~50 MB
failover-connector After the ~20 s primary buffer is exhausted, the connector trips and traffic is routed to the Failover pipeline for the duration of both the 1 h and 2 h scenarios. Instant — primary drains as soon as backend is healthy; rejoin window is negligible. Always — it’s the “healthy path” before failover engages.
failover_queue sending_queue + file_storage on PVC · varies by row
10 000 false ~3 m 20 s
(200 s)
~500 MB saved: 10 000
dropped: ~170 000
Queue fills in 3 m 20 s; remaining ~56 m drops newest batches at the exporter. enqueue_failed_* rises steadily.
saved: 10 000
dropped: ~350 000
Same pattern; drop window extends ~1 h 57 m.
~3 m 20 s
(drain the 10 k backlog while live ingest continues at 50/s — see drain-rate note below)
Tight PVC budget; short-outage tolerance only; bursty workload where newest-data loss is acceptable.
10 000 true ~3 m 20 s — then stalls ~500 MB saved: 10 000
blocked upstream: ~170 000
After fill, exporter blocks. Backpressure propagates to receiver and OTLP clients; they retry or drop themselves.
saved: 10 000
blocked upstream: ~350 000
Upstream bears the cost for ~1 h 57 m.
~3 m 20 s Producers can hold or retry themselves; collector must never drop.
15 000 false ~5 m
(300 s)
~750 MB saved: 15 000
dropped: ~165 000
Queue fills in 5 m; dropping window ~55 m.
saved: 15 000
dropped: ~345 000
Dropping window ~1 h 55 m.
~5 m Moderate PVC budget; balances buffering with disk cost.
15 000 true ~5 m — then stalls ~750 MB saved: 15 000
blocked upstream: ~165 000
saved: 15 000
blocked upstream: ~345 000
~5 m Upstream can tolerate ~55 m of backpressure on a 1 h outage.
20 000 false ~6 m 40 s
(400 s)
~1.0 GB saved: 20 000
dropped: ~160 000
Dropping window ~53 m.
saved: 20 000
dropped: ~340 000
Dropping window ~1 h 53 m.
~6 m 40 s Sweet spot when PVC cost and headroom both matter.
20 000 true ~6 m 40 s — then stalls ~1.0 GB saved: 20 000
blocked upstream: ~160 000
saved: 20 000
blocked upstream: ~340 000
~6 m 40 s Strong durability; upstream is resilient to ~53 m backpressure on 1 h.
25 000 false ~8 m 20 s
(500 s)
~1.25 GB saved: 25 000
dropped: ~155 000
Dropping window ~51 m 40 s.
saved: 25 000
dropped: ~335 000
Dropping window ~1 h 51 m.
~8 m 20 s Max headroom with bounded drop behaviour; watch PVC compaction.
25 000 true ~8 m 20 s — then stalls ~1.25 GB saved: 25 000
blocked upstream: ~155 000
saved: 25 000
blocked upstream: ~335 000
~8 m 20 s Strongest single-node durability; requires resilient upstream.

Can the failover drain be rate-limited? — Yes.

The collector exposes several knobs to throttle how fast the Failover pipeline exports its backlog after the aggregator returns:

  • sending_queue knobs: num_consumers (worker count) directly scales the drain rate — halving it halves throughput. Default is 10 workers.
  • Pipeline processors: insert a rate_limiter / tail_sampling / transform processor before the exporter to clamp outbound rate.
  • Extension: the memory_limiter extension can shed load when the collector is hot, indirectly throttling drain.
  • Batch + timeout: a batch processor with a larger timeout + smaller send_batch_size reduces effective req/s to the backend.
  • Exporter settings: otlp exporter’s sending_queue.num_consumers, and client compression / max_request_size all shape throughput.

The matrix below shows effective drain performance at full rate, 1/2 rate, and 1/3 rate — the key subtlety is that live ingest is still arriving at 50 batches/s, so only a drain rate greater than ingest actually shrinks the backlog.

Failover drain rate vs live ingest

Once the aggregator returns, the failover exporter starts draining. If the exporter is rate-limited, the effective backlog reduction per second = drain_rate − ingest_rate. Any configuration where drain ≤ ingest means the backlog never shrinks while producers are live — catch-up only happens when ingest falls below the drain rate.

Drain rate with and without rate limiting · live ingest = 50 batches/s Assumes a 25 000-batch backlog carried out of a 1-hour outage (worst-case saved amount). “Catch-up time” is measured while live ingest continues at 50/s.
Drain config Effective drain rate Net drain
(drain − 50/s ingest)
Catch-up time for 25 000-batch backlog Notes
Full rate
num_consumers ≥ 10 · no limit
~50 batches/s 0 / s (break-even) never — while live ingest = 50/s
Only drains once ingest dips; if ingest falls to 25/s, catch-up ≈ 16 m 40 s.
Break-even default. Good enough once traffic is nightly-low.
2× rate
num_consumers doubled · higher parallelism
~100 batches/s +50 / s ~8 m 20 s If the backend/network can keep up, this is the fastest safe catch-up.
3× rate
aggressive drain · bursty
~150 batches/s +100 / s ~4 m 10 s Fastest — risk of saturating aggregator / inducing a second outage.
Half rate
num_consumers halved · rate_limiter 25/s
~25 batches/s −25 / s (backlog grows) never — backlog grows by 25/s
PVC fills again at 25/s × remaining outage-budget; same drop/block behaviour returns.
Only safe if ingest is simultaneously throttled (rate_limiter on ingress).
Third rate
rate_limiter ~16.7/s
~16.7 batches/s −33.3 / s (backlog grows faster) never — backlog grows by 33/s Hazardous with live ingest; usable only during post-incident off-hours.
Quarter rate
rate_limiter ~12.5/s
~12.5 batches/s −37.5 / s never — backlog grows by 37.5/s Effectively non-draining under live load — avoid unless ingest is paused.

Practical guidance

  • Size PVC for outage duration, not loss tolerance. A 1 h outage at 50 batches/s = 180 000 batches = ~9 GB. No queue_size in this table buffers a full 1 h without drops or blocking.
  • Prefer block_on_overflow = true if OTLP clients can buffer; prefer false if they can’t and newest-data loss is acceptable.
  • For safe catch-up, configure drain rate greater than steady ingest. At 50 batches/s sustained, target ≥ 100 batches/s drain until the backlog is gone — then revert to a conservative rate to protect the aggregator.
  • Combine drain limiting with ingest limiting. A rate_limiter processor on the failover pipeline’s ingress (not just the exporter) is the only way a below-ingest drain rate actually reduces backlog.