open-telemetry / open-telemetry/opentelemetry-cpp
[SDK] `BatchSpanProcessor` drain the queue in a tight loop instead of waiting for the next full batch
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 1.4k
- Forks
- 632
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 75
Description
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_receiverlistening onlocalhost:2026(same machine, negligible network latency) - BatchSpanProcessor configuration:
max_queue_size = 8192max_export_batch_size = 2048schedule_delay_millis = 5000
Steps to reproduce
- Start a local OTLP gRPC receiver that exposes per-export counters.
- Configure
BatchSpanProcessorwithOtlpGrpcExporterpointing 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_delayexpires.
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_sizeorschedule_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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with BatchSpanProcessor::Export() and review how its export loop consumes the queue after each OtlpGrpcExporter request. Compare the current behavior with the requested strict-batching semantics and determine how completion should be verified for full batches, schedule delays, and existing flush behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- observability-sre
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100