Describe your environment
- Platform: macOS (Apple Silicon)
- opentelemetry-cpp version: 1.27.0 (installed via Homebrew)
- Compiler: AppleClang 16.0.0
- Downstream collector: local
otlp_receiver listening on localhost:2026 (same machine, negligible network latency)
- BatchSpanProcessor configuration:
max_queue_size = 8192
max_export_batch_size = 2048
schedule_delay_millis = 5000
Steps to reproduce
- Start a local OTLP gRPC receiver that exposes per-export counters.
- Configure
BatchSpanProcessor with OtlpGrpcExporter pointing at the local receiver.
- Produce spans at a constant rate (10k / 30k / 50k spans/s) for 60 seconds.
- Observe the number of export requests received and the average number of spans per request.
What is the expected behavior?
With max_export_batch_size = 2048, a steady stream of spans should result in exports that are close to 2048 spans each. This minimizes gRPC request count and CPU overhead.
This is consistent with the BSP semantics in Java and Go:
- Export when a full batch is available.
- If a full batch is never reached, export whatever is buffered when
schedule_delay expires.
What is the actual behavior?
BatchSpanProcessor::Export() in opentelemetry-cpp is effectively a tight do { ... } while(true) loop:
void BatchSpanProcessor::Export()
{
do {
num_records_to_export =
buffer_.size() >= max_export_batch_size_ ? max_export_batch_size_ : buffer_.size();
// ... consume & export ...
} while (true);
}
After each export finishes, the worker immediately checks the buffer again. As long as the buffer is non-empty, it exports another batch. It does not wait for the next 2048-span batch to accumulate.
As a result, even with a local fast collector, the average batch size is only around 1300–1400 spans instead of 2048. The extra export requests cause more serialization, more lock/condition-variable wakeups, and more gRPC calls, leading to higher CPU usage than a strict-batch implementation.
Performance data
The following data was collected with 2048 batch size, 0 ms added export delay, and no explicit ForceFlush.
(2026.8.19 edit, The previous tests had noise; the CPU core differences were actually not significant.)
| sps |
mode |
export requests |
avg spans/request |
| 10,000 |
official BSP |
416 |
1,442.3 |
| 10,000 |
strict-batch BSP |
293 |
2,047.8 |
| 30,000 |
official BSP |
1,309 |
1,375.1 |
| 30,000 |
strict-batch BSP |
879 |
2,047.8 |
| 50,000 |
official BSP |
2,269 |
1,322.2 |
| 50,000 |
strict-batch BSP |
1,465 |
2,047.8 |
Notes:
- The official BSP only reaches about 65%–70% of the configured batch size, and issues roughly 35%–55% more export requests than strict-batch.
Questions / Discussion
- What is the rationale for the current tight-loop drain behavior? Is it primarily to reduce export latency, or is there another reason?
- Has the project considered an optional mode where the worker exports exactly one batch per wakeup and then waits again for either
max_export_batch_size or schedule_delay?
- For CPU-sensitive deployments that can tolerate the 5-second maximum delay, the current behavior does not appear optimal. Would the community be open to a PR that adds a stricter batching strategy?
Additional context
- This behavior occurs on the normal export path and does not require
ForceFlush.
- When exporter latency is increased (simulating a slow remote collector), the official BSP's average batch size does approach 2048, but only because the queue starts backing up and dropping spans. The strict-batch implementation continues to export 2048-span batches stably under the same conditions.
Describe your environment
otlp_receiverlistening onlocalhost:2026(same machine, negligible network latency)max_queue_size = 8192max_export_batch_size = 2048schedule_delay_millis = 5000Steps to reproduce
BatchSpanProcessorwithOtlpGrpcExporterpointing at the local receiver.What is the expected behavior?
With
max_export_batch_size = 2048, a steady stream of spans should result in exports that are close to 2048 spans each. This minimizes gRPC request count and CPU overhead.This is consistent with the BSP semantics in Java and Go:
schedule_delayexpires.What is the actual behavior?
BatchSpanProcessor::Export()in opentelemetry-cpp is effectively a tightdo { ... } while(true)loop:After each export finishes, the worker immediately checks the buffer again. As long as the buffer is non-empty, it exports another batch. It does not wait for the next 2048-span batch to accumulate.
As a result, even with a local fast collector, the average batch size is only around 1300–1400 spans instead of 2048. The extra export requests cause more serialization, more lock/condition-variable wakeups, and more gRPC calls, leading to higher CPU usage than a strict-batch implementation.
Performance data
The following data was collected with 2048 batch size, 0 ms added export delay, and no explicit
ForceFlush.(2026.8.19 edit, The previous tests had noise; the CPU core differences were actually not significant.)
Notes:
Questions / Discussion
max_export_batch_sizeorschedule_delay?Additional context
ForceFlush.