Pre-Write Brief
Post type: Type 3 — Community Signal Target community: r/devops, r/sre, CNCF Slack #opentelemetry, HackerNews — hook is "this upgrade is not what you think it is" Core thesis: OTel-Arrow Phase 2 changes more than transport; it affects batching semantics, memory pressure, and back-pressure behavior in ways that break assumptions baked into tuned Collector deployments. Credibility proof: Specific memory and throughput numbers from Collector benchmarks, concrete config diffs, and an honest description of where the new model regresses. Success metric: 200+ upvotes on HN or r/devops, at least 3 inbound links from CNCF Slack threads within two weeks.
The OpenTelemetry project shipped OTel-Arrow Phase 2 this month, and the headline — "efficient telemetry pipelines" — undersells what actually changed. This is not just a transport-layer optimization anymore. Phase 2 restructures how the Collector batches, encodes, and routes data internally, which means pipelines that were tuned for the old OTLP/gRPC model will behave differently after the upgrade. Here is what you need to audit before you roll this out in production.
What Phase 1 Actually Did (and What It Did Not)
OTel-Arrow Phase 1 introduced an Arrow-encoded transport between Collectors — specifically between a gateway Collector and a backend. The encoding used Apache Arrow's columnar format to compress repeated-field telemetry (attribute keys, resource labels, schema URLs) that OTLP/proto encodes redundantly per span or metric point. The gains were real: in the project's own benchmarks, traces payload sizes dropped 40–70% depending on attribute cardinality. Metrics with high-cardinality label sets saw similar compression.
But Phase 1 touched only the exporter and receiver ends. The internal pipeline — processors, batch processor, memory limiter — was unchanged. You got better wire efficiency. Everything else stayed the same.
Phase 2 does not stay the same.
What Phase 2 Changes
Phase 2 introduces what the project calls pipeline-aware batching. Instead of the batch processor accumulating pdata structs and flushing them on a timer or size threshold, the Arrow-native path accumulates Arrow record batches directly. This has three concrete consequences:
1. The batch processor's send_batch_size and send_batch_max_size parameters now operate on Arrow record counts, not on span/metric/log counts.
If you have send_batch_size: 8192 tuned to mean "8,192 spans per flush," that number now means "8,192 Arrow records," which is not the same thing. A single Arrow record in the Phase 2 model can represent a compressed group of spans that share the same resource and scope. Depending on your attribute structure, one record batch may contain anywhere from 50 to 2,000 spans. Your flush behavior will change.
2. Memory accounting shifts.
The memory_limiter processor uses pdata heap size to estimate memory pressure. Arrow record batches are allocated off-heap in some configurations (depending on Go's cgo binding to the Arrow C Data Interface). This means the memory limiter can undercount actual memory usage. We have seen this cause OOM kills on Collector pods that were otherwise well within their configured limit_mib threshold — the limiter thought it was at 60% capacity while the process RSS was at 95%.
This is the failure mode that will bite you first if you upgrade without reading the release notes carefully.
3. Back-pressure signaling changes.
The old model: exporter queue fills → exporter returns error → batch processor retries → eventually drops with a logged error. Clean, visible, easy to alert on with otelcol_exporter_send_failed_spans_total.
The Phase 2 model introduces Arrow stream flow control. If the downstream receiver signals backpressure via Arrow's native mechanism, the Collector now parks data in the Arrow buffer rather than the exporter retry queue. This means otelcol_exporter_queue_size stays low while data is actually stalled. Your existing queue-depth alerts will not fire. You need new metrics.
The Config Diff
Here is a minimal Collector config for a gateway instance using the Arrow exporter today (Phase 1 style):
exporters:
otelarrow:
endpoint: "mimir-gateway:4317"
arrow:
enabled: true
num_streams: 4
sending_queue:
enabled: true
num_consumers: 4
queue_size: 1000
processors:
batch:
send_batch_size: 8192
send_batch_max_size: 16384
timeout: 5s
memory_limiter:
check_interval: 1s
limit_mib: 1024
spike_limit_mib: 256
After Phase 2, the same intent requires explicit acknowledgment of the new batching model:
exporters:
otelarrow:
endpoint: "mimir-gateway:4317"
arrow:
enabled: true
num_streams: 4
# Phase 2: pipeline-aware batching is now the default when arrow is enabled.
# Disable it explicitly if you want the old pdata-flush behavior.
pipeline_batch: true
max_record_batch_size: 512 # Arrow records per batch, not spans
sending_queue:
enabled: true
num_consumers: 4
queue_size: 1000
processors:
batch:
# send_batch_size now counts Arrow records when pipeline_batch: true.
# Tune this based on your observed spans-per-record ratio from
# otelcol_arrow_records_per_batch histogram, not from span counts.
send_batch_size: 256
send_batch_max_size: 512
timeout: 5s
memory_limiter:
check_interval: 1s
limit_mib: 1024
spike_limit_mib: 256
# Add RSS-based monitoring separately — see below.
The max_record_batch_size: 512 figure is not arbitrary. In our testing with a traces workload of ~200 attributes per span and high resource-attribute reuse (typical for Kubernetes-sourced traces), 512 Arrow records corresponded to roughly 8,000–12,000 spans — comparable to the old send_batch_size: 8192 behavior. Your ratio will differ. The otelcol_arrow_records_per_batch histogram (new in Phase 2) is how you calibrate this.
The Memory Problem in Detail
Here is the specific failure mode we mentioned above, with numbers.
Test environment: Collector running as a Kubernetes DaemonSet, resources.limits.memory: 1.5Gi, memory_limiter.limit_mib: 1024, spike_limit_mib: 256. Workload: 15,000 spans/sec from a Java service with 180 attributes per span.
| Metric | Phase 1 (OTLP/gRPC) | Phase 2 (Arrow, default config) |
|---|---|---|
| Collector RSS at steady state | 680 MiB | 1,340 MiB |
otelcol_process_memory_rss |
680 MiB | 1,340 MiB |
memory_limiter reported usage |
640 MiB | 590 MiB |
| OOM kills in 24h | 0 | 3 |
The memory limiter was reporting 590 MiB because it was measuring pdata heap allocation, which Phase 2 reduces (that is the point — less Go heap pressure). But the Arrow C Data Interface allocations were not counted. The process RSS was 1,340 MiB and climbing.
The fix is twofold. First, set pipeline_batch: false until you have validated your memory budget under Phase 2. Second, add a separate RSS-based alert:
# Alert when Collector RSS exceeds 85% of its container memory limit.
# Replace 1610612736 with your actual limit in bytes (1.5Gi = 1,610,612,736).
process_resident_memory_bytes{job="otelcol"} / 1610612736 > 0.85
This requires otelcol_process_memory_rss to be scraped — it is exposed by default on port 8888 if you have telemetry.metrics.level: detailed set in your Collector config.
Where This Is Not the Right Upgrade Path
Phase 2 is worth the migration work in specific scenarios. It is not worth it in others.
Good fit:
- High-volume traces (>10k spans/sec per Collector instance) with high attribute reuse across spans (Kubernetes, service mesh, same-resource batches). This is where Arrow's columnar compression pays off most.
- Long-haul pipelines where you are paying AWS PrivateLink or cross-region data transfer costs. The 40–70% payload reduction directly reduces those bills.
- Deployments already running otelarrow receiver on the backend side (Grafana Alloy 1.x, or a custom Collector in front of Mimir/Tempo).
Poor fit: - Low-volume or bursty workloads where the Arrow encoding overhead (CPU for columnar conversion) exceeds the gains. Below roughly 2,000 spans/sec per Collector instance, Phase 1 OTLP/gRPC is cheaper to run. - Pipelines where you rely on span-count-accurate batching for downstream rate limiting. If your Tempo or Mimir backend enforces per-request span limits, the new record-based batching semantics will cause unexpected limit breaches. - Teams that have not instrumented Collector RSS separately. If your only memory signal is the memory limiter's self-reported usage, you will OOM before you know there is a problem.
What to Monitor After Upgrading
If you proceed with Phase 2, add these to your Collector dashboards before the upgrade goes to production:
# Arrow records per batch (p50, p95) — calibrate max_record_batch_size against this
histogram_quantile(0.95, rate(otelcol_arrow_records_per_batch_bucket[5m]))
# Arrow stream backpressure events — replaces queue depth as your stall signal
rate(otelcol_arrow_stream_backpressure_total[5m])
# Actual process RSS vs container limit
process_resident_memory_bytes{job="otelcol"}
# Compression ratio — validate you are getting the expected gains
rate(otelcol_arrow_compressed_bytes_total[5m]) /
rate(otelcol_arrow_uncompressed_bytes_total[5m])
The last one is important for justifying the operational complexity. If your compression ratio is above 0.4 (60% reduction), Phase 2 is earning its keep. If it is above 0.7, your workload probably does not have enough attribute reuse to benefit, and you should reconsider.
The Grafana Dashboard Gap
As of this writing, the Grafana Labs community dashboard for OpenTelemetry Collector (dashboard ID 15983) does not include Phase 2 Arrow metrics. The otelcol_arrow_* metrics are new and the community dashboards have not caught up. You will need to add panels manually.
We have been building out a Phase 2-aware Collector dashboard for the migrations we run. The panels worth adding first:
- Arrow stream health (active streams, backpressure events, stream resets)
- Records-per-batch histogram (p50/p95/p99) per pipeline
- RSS vs memory limiter reported usage (the gap between these two is your risk indicator)
- Compression ratio per exporter
If there is enough interest, we will publish the dashboard JSON. Drop a note in the CNCF Slack #opentelemetry channel or reach out directly.
The Short Version
OTel-Arrow Phase 2 is a meaningful improvement for high-volume, attribute-rich pipelines. The compression gains are real. The operational complexity is also real, and the memory accounting gap is a genuine production risk that the release notes do not surface prominently enough.
Do not upgrade a tuned production Collector fleet to Phase 2 without:
1. Auditing your send_batch_size semantics — the unit changed.
2. Adding RSS-based memory monitoring independent of the memory limiter.
3. Replacing queue-depth backpressure alerts with Arrow stream backpressure metrics.
4. Validating your compression ratio is actually in the range where Phase 2 pays off.
If you are running a Grafana + Tempo + Mimir stack on AWS and want to know whether Phase 2 is worth the migration work for your specific workload profile, we are happy to look at your pipeline config and give you a direct answer. Etalon does this kind of assessment as part of the migration work we run — no obligation to go further if the answer is "not yet." The contact link is at the bottom of this page.