How Is Batching Tuned at Scale in OpenTelemetry?
A plain-language guide to the queues, timers, batch sizes and safety valves that decide whether your traces, metrics and logs arrive on time, arrive complete, or never arrive at all.
Every request your application serves leaves behind a small trail of evidence: a span that says how long one step took, a log line that says what happened, a metric point that says how busy things were. OpenTelemetry, usually shortened to OTel, is the open-source toolkit that collects this evidence and ships it to a backend where people can search and chart it. Shipping sounds easy until you do the arithmetic. A busy service can create thousands of spans every second, and sending each one in its own network call would cost more effort than the real work the service is doing.
The fix is called batching: gather many items, then send them together. The hard part is deciding how many items, how often, how much memory to spend while waiting, and what to do when the destination slows down. This guide walks through every layer where OpenTelemetry batches data, explains each setting in simple words, and shows how engineers tune those settings when traffic grows from hundreds of items per second to hundreds of thousands.
You do not need to know anything about OpenTelemetry to follow along. Every new term is explained the first time it appears, and each big idea gets an everyday comparison before the technical detail.
AWhy Batching Exists in the First Place
Sending data one piece at a time feels natural, and it is almost always the wrong choice once traffic grows.
What Is Telemetry?
Telemetry is the data a running system produces about itself so that humans can understand it from the outside. OpenTelemetry works with three kinds of it, usually called signals. Traces record the journey of one request through many services, and each step in that journey is a span. Metrics are numbers measured over time, such as requests per second or memory in use. Logs are timestamped text records of events.
All three signals share one problem. They are produced constantly, in tiny pieces, by code that is busy doing something else. A checkout service has no time to stop and negotiate with a monitoring server every time a step finishes.
The Hidden Cost of Sending One Item at a Time
Imagine a single span. After it is encoded into bytes it may be only a few hundred bytes long. Now picture what it takes to send that span alone. The program must turn the span into a wire format, wrap it in a network message with headers, hand it to the operating system, wait for the other side to acknowledge it, and then clean up. Each of those steps has a fixed price that does not shrink just because the payload is small.
The server on the other end pays too. It must accept the connection or reuse one, check who is calling, route the request, parse the bytes, and send back a reply. When millions of tiny requests arrive, the server spends most of its energy on ceremony instead of storing data.
Think of a courier company. If every parcel went out in its own van, the roads would jam and the fuel bill would explode, even though each parcel is small. Real couriers fill a truck, leave when it is full or when the scheduled departure time arrives, and deliver dozens of parcels in one trip. OpenTelemetry batching is exactly this: the van is the network call, the parcels are spans, and the departure schedule is the timer.
What Batching Buys You
- Fewer network calls. One call carries hundreds of items, so the fixed price is shared by all of them.
- Better compression. Compression finds repeated patterns, and a batch of spans from the same service repeats names, attribute keys and resource details constantly. Bigger payloads shrink more.
- Less CPU. Encoding, system calls and thread wake-ups happen once per batch, not once per item.
- A calmer backend. A steady stream of medium-sized requests is easier to ingest than a storm of tiny ones.
- Work off the hot path. The application only drops the item into an in-memory queue, which is very fast. A separate background worker does the slow network part.
What Batching Costs You
Nothing is free. While items wait for their batch to fill, they are not visible to anyone, so batching adds delay. While they wait they also sit in memory, so batching spends RAM. And if the process crashes before the batch is sent, those items are gone, so batching adds a small risk of loss. Tuning at scale is the art of balancing these three costs against the efficiency you gain.
Bigger batches and longer waits make the pipeline cheaper but slower and a little riskier. Smaller batches and shorter waits make it faster and safer but more expensive. There is no universally correct setting, only a setting that fits your traffic.
Why the Defaults Stop Working at Scale
The built-in defaults were chosen to be safe for a modest service. They work well for a few hundred spans per second. At tens of thousands of items per second, the same defaults can overflow their queues, send tiny batches during quiet hours, send oversized batches that the receiver rejects, or let memory grow until the process is killed. Each of these failures has a clear cause, and the rest of this guide explains them one by one.
BThe Telemetry Journey: Where Batching Lives
Batching is not one setting in one place. It happens at several points along the road from your code to your dashboard.
Meet the Cast
Instrumentation is the code that creates telemetry, either written by hand or added automatically by a library. The SDK (software development kit) is the OpenTelemetry library inside your application that receives that telemetry, decides what to keep, and prepares it for shipping. Inside the SDK, a processor handles each item as it finishes, and an exporter is the part that actually sends data out of the process.
The exporter usually speaks OTLP, the OpenTelemetry Protocol, which is the standard format and wire protocol for telemetry. OTLP can travel over gRPC (a fast, binary, HTTP/2-based protocol, usually on port 4317) or over plain HTTP carrying Protobuf (usually on port 4318).
The receiver of that data is often the OpenTelemetry Collector, a separate program whose whole job is to receive, process and forward telemetry. A Collector is built from three kinds of parts. Receivers accept data. Processors change or filter it. Exporters send it onward. The chain of those parts for one signal is called a pipeline.
At scale, teams run two layers of Collectors. An agent Collector runs close to the application, for example one per server. A gateway Collector is a shared pool of Collectors that sits in front of the backend. The backend is the database and user interface where people finally look at the data.
flowchart LR A["Application code
creates spans, logs and metrics"] --> B["SDK batch processor
queue plus timer"] B -->|"OTLP over gRPC or HTTP"| C["Agent Collector
one per node"] C -->|"OTLP"| D["Gateway Collector pool
batch processor plus exporter queue"] D -->|"Backend protocol"| E["Backend
storage and query"]
The Five Places Batching Happens
Follow one span along the diagram above and you will meet batching five times. First, the SDK processor holds it in a queue until a batch is ready. Second, the exporter sends the batch as one request. Third, the Collector’s batch processor regroups what arrives from many senders into new batches. Fourth, the Collector’s exporter keeps its own queue of batches waiting to be sent, and may retry them. Fifth, the backend often buffers incoming data before writing it to storage.
| Layer | What is grouped | Who controls it |
|---|---|---|
| SDK batch processor | Finished spans or log records | Application settings or environment variables |
| SDK metric reader | Aggregated metric points per interval | Export interval setting |
| Collector batch processor | Items from many senders | Collector configuration file |
| Collector exporter queue | Ready-to-send requests | Collector configuration file |
| Backend ingestion buffer | Incoming writes | Backend vendor or operator |
Settings at different layers must agree with each other. A Collector that rejects any request larger than 4 MiB makes a huge SDK batch useless, and a gateway batch timeout of 10 seconds makes a 1-second SDK delay pointless for freshness. Always tune with the whole path in mind.
A Brief History That Explains the Design
Large companies hit these problems long before OpenTelemetry existed. Google’s Dapper paper, published in 2010, described tracing a huge fleet by sampling only a small fraction of requests and collecting the data out of band, so that the monitored applications were never slowed down. Uber built Jaeger for the same reason and later gave it to the Cloud Native Computing Foundation. In 2019 two competing projects, OpenTracing and OpenCensus, merged to form OpenTelemetry. The lessons from those years live on in its design: keep the hot path tiny, send in groups, and protect the application first.
CInside the SDK: How a Batch Processor Works
The first and most important batching layer lives inside your own application process.
Simple Versus Batch
OpenTelemetry offers two processors for spans. The simple processor sends each span the moment it ends, and it waits for the send to finish. That is fine on a laptop while you are debugging, but in production it would put a network call on the path of every request. The batch processor is the production choice. It accepts a finished span almost instantly and does the sending later on a background worker.
The Life of One Span
Here is what happens when a span ends. If a sampler already decided to discard it, it never reaches the processor, so sampling is your first and cheapest way to reduce load. Otherwise the processor tries to place the span into a bounded queue, which is a waiting line with a fixed maximum length. If the line is full, the span is dropped on the spot and a counter or warning notes the loss. If there is room, the span joins the line and the application moves on, usually within microseconds.
A background worker watches the queue. It wakes up in two situations: when the queue holds at least one full batch, or when the schedule timer fires. It then removes up to one batch worth of spans, hands them to the exporter, and waits for the exporter to finish or for a time limit to pass. Then it checks the queue again.
flowchart TD
S["Span ends"] --> Q{"Is the queue full?"}
Q -->|"Yes"| X["Drop the span and record the loss"]
Q -->|"No"| W["Add span to the queue"]
W --> T{"Full batch waiting
or timer fired?"}
T -->|"No"| K["Keep waiting"]
T -->|"Yes"| B["Remove up to one batch"]
B --> E["Exporter sends the batch
waits for reply or timeout"]
E --> T
A shuttle bus at an airport follows two rules: leave when every seat is taken, or leave at the scheduled time even if seats are empty. The waiting area has limited space, so when it is completely packed, newcomers are turned away. The queue is the waiting area, the batch size is the number of seats, and the scheduled delay is the departure timetable.
The Hidden Speed Limit
Most SDKs send one batch at a time. The specification says the processor must not call the exporter concurrently, so a new batch cannot leave until the previous one has been answered. That creates a speed limit that surprises many teams. The maximum steady throughput of one processor is roughly the batch size divided by the time one export takes.
Take the default batch size of 512 spans and a network round trip to the Collector of 200 milliseconds. One export per 0.2 seconds gives about five exports per second, or roughly 2,560 spans per second. A service that creates 5,000 spans per second will fill its queue faster than it can drain it, no matter how large the timer settings are. The queue fills, spans start dropping, and nothing looks wrong on the application side.
Raising the queue size alone does not fix a throughput problem. A bigger queue only delays the overflow. If the drain rate is lower than the arrival rate, the queue fills eventually. To fix it, increase the batch size, reduce export latency by placing a Collector closer, or lower the volume with sampling.
Flushing on Shutdown
When an application stops, the spans still sitting in the queue would vanish unless someone sends them. A well-behaved SDK offers a flush operation that forces an immediate export and a shutdown operation that flushes and then stops the worker. Long-running servers call shutdown when they receive a stop signal. Short-lived programs, such as batch jobs and serverless functions, must call flush before they finish, or their final spans are lost.
DThe Four Knobs and What They Really Do
Almost every batching decision in the SDK comes down to four numbers.
Maximum queue size
How many items may wait. Default 2,048. Controls how much of a burst or backend stall you can absorb before dropping data.
Maximum export batch size
How many items go in one request. Default 512. Sets the throughput ceiling and the size of every network message.
Scheduled delay
How long to wait before sending a partial batch. Default 5 seconds for spans, 1 second for logs. Sets how stale your data can be.
Export timeout
How long one export may run before it is cancelled. Default 30 seconds. Protects the worker from hanging forever.
Names and Defaults
These settings can be changed in code or, more conveniently, through environment variables that every standard SDK understands. Using environment variables means operators can retune a service without rebuilding it.
| Knob | Span variable | Log variable | Default |
|---|---|---|---|
| Queue size | OTEL_BSP_MAX_QUEUE_SIZE | OTEL_BLRP_MAX_QUEUE_SIZE | 2,048 |
| Batch size | OTEL_BSP_MAX_EXPORT_BATCH_SIZE | OTEL_BLRP_MAX_EXPORT_BATCH_SIZE | 512 |
| Scheduled delay | OTEL_BSP_SCHEDULE_DELAY | OTEL_BLRP_SCHEDULE_DELAY | 5,000 ms spans, 1,000 ms logs |
| Export timeout | OTEL_BSP_EXPORT_TIMEOUT | OTEL_BLRP_EXPORT_TIMEOUT | 30,000 ms |
How the Knobs Interact
The knobs are not independent, and most tuning mistakes come from changing one while forgetting the others.
Batch size must not exceed queue size. A batch is taken out of the queue, so a queue smaller than a batch could never produce a full one. In practice the queue should hold many batches, so that new items can keep arriving while one batch is out being sent. A common rule of thumb is a queue several times larger than the batch.
Delay matters mostly when traffic is low. At high traffic, the queue reaches a full batch long before the timer fires, so batches leave because they are full. At low traffic, the timer is what releases batches, and they leave partly empty. This is why a service that sends perfect 512-span batches at noon may send batches of 12 at 3 a.m.
Timeouts form a chain. The OTLP exporter has its own request timeout, 10 seconds by default, separate from the processor’s 30-second export timeout. If the exporter timeout is longer than the processor timeout, the processor gives up while the request is still running. Keep the inner timeout shorter than the outer one so that failures are reported by the layer that understands them.
When you change a knob, write down the arrival rate, the export latency and the expected stall time you designed for. Settings without those numbers are guesses, and the next engineer cannot tell whether they still make sense.
Example: a payment API during a sale
A payment service handles 1,500 requests per second and creates about four spans per request, so 6,000 spans per second. With the default batch of 512 and a 150 ms round trip, the processor can drain only about 3,400 spans per second. Raising the batch size to 2,048 lifts the ceiling to roughly 13,000 per second, and a queue of 16,384 gives about two and a half seconds of cushion if the Collector stalls.
EMetrics and Logs Play by Different Rules
Spans arrive one at a time and wait in a queue. The other two signals batch in their own ways, and each has its own scaling trap.
Metrics: Summaries Instead of Receipts
A metric is not stored as a long list of individual events. When your code records that a request took 120 milliseconds, the SDK does not keep that single event. It folds the value into a running summary in memory, for example a count, a sum and a set of histogram buckets. Think of a shop that does not keep every receipt on the counter but updates a tally sheet instead.
The part that exports these summaries is the periodic exporting metric reader. Every time its interval ends, it collects the current summaries of all metrics, packs them into one request, and sends them. The interval defaults to 60 seconds and can be changed with OTEL_METRIC_EXPORT_INTERVAL. The time allowed for one export defaults to 30 seconds and is set with OTEL_METRIC_EXPORT_TIMEOUT.
A shop manager can either hand the owner a receipt for every sale, or hand over one summary sheet at the end of each hour. The summary sheet is much smaller and easier to deliver, but the owner only learns what happened once an hour. A shorter interval gives fresher numbers and more paperwork.
The Real Size Driver Is Cardinality
Cardinality means the number of unique combinations of attribute values for a metric. A request counter with the attribute “HTTP method” has a handful of combinations. Add “user identifier” and it has one combination for every user, which could be millions. Each combination becomes its own time series and its own data point in every export.
Here is the arithmetic that surprises people. Suppose a fleet has 5,000 pods and each pod reports 500 series. That is 2.5 million data points per interval. With a 60-second interval, the pipeline carries about 41,700 points per second. Cut the interval to 10 seconds for fresher dashboards and the same fleet now sends 250,000 points per second, six times more, without a single extra user.
Teams often shorten the metric interval to make graphs feel live, then wonder why the Collector bill tripled. Before changing the interval, ask whether anyone makes a decision within those extra seconds. Alerts rarely need data fresher than 30 to 60 seconds.
The Synchronized Burst Problem
Metric readers export on a fixed rhythm that starts when the process starts. When a whole fleet restarts at the same moment, for example during a fast deployment, thousands of readers share the same rhythm. Every minute the Collector sees a sudden wall of data followed by silence. Rolling out changes gradually, or adding a small random delay at startup, spreads the load into a smooth stream.
Logs: Spans With a Temper
Log records use a batch log record processor that works like the span processor: a bounded queue, a batch size, a delay and a timeout. Its default delay is shorter, one second, because people expect log lines to appear quickly. The settings use the OTEL_BLRP_ prefix.
The danger with logs is that they are bursty in exactly the wrong way. When a dependency fails, every request starts writing error logs, so volume can jump tenfold at the same instant the system is struggling and the backend may be slow. A queue sized for a normal day overflows during the incident, and the lines you most wanted vanish first. Sizing the log queue for the bad day, and filtering noisy low-severity lines at the source, protects the records that matter.
| Signal | How items are grouped | Default freshness | Main scaling risk |
|---|---|---|---|
| Spans | Queue, released by size or timer | Up to 5 seconds | Queue overflow when export is too slow |
| Logs | Queue, released by size or timer | Up to 1 second | Burst during incidents |
| Metrics | Summaries exported every interval | Up to 60 seconds | Cardinality and synchronized bursts |
FThe Collector Batch Processor
The SDK groups data from one process. The Collector regroups data from thousands of processes into batches that suit the backend.
Why Batch a Second Time?
The senders in front of a Collector all make their own choices. One sends 12 spans because it is quiet, another sends 512 because it is busy, and a third sends a single span because someone configured a simple processor. If the Collector forwarded each request as it arrived, the backend would see the same messy mix.
The Collector’s batch processor fixes this by pouring everything into one container and handing the exporter evenly sized batches. It does not care who sent the data. It cares only about how many items it holds and how long the oldest item has been waiting.
A warehouse receives boxes from many small suppliers, some with one item and some with fifty. Workers do not ship each supplier’s box separately. They consolidate everything onto full pallets and send the pallets out. Pallets are cheaper to move because every truck leaves nearly full.
The Settings That Matter
send_batch_size
The number of items that makes the processor send a batch immediately. Default 8,192. Counted in items such as spans, log records or metric data points, not in bytes.
timeout
How long the processor waits before sending whatever it holds, even if the batch is small. Default 200 milliseconds.
send_batch_max_size
An upper limit on batch size. Default 0, meaning no limit. When set, bigger batches are split. It must be at least as large as send_batch_size.
metadata_keys
Names of request headers or metadata used to keep separate batches per value, for example per tenant. A companion setting limits how many distinct groups are allowed.
Size Is a Trigger, Not a Cap
This detail causes real incidents. The send_batch_size setting says “send when you reach at least this many”. If a sender delivers one request holding 20,000 spans, the processor adds all 20,000 and sends a batch that big. The setting did not stop it. Only send_batch_max_size splits batches into guaranteed maximum sizes. When your backend or next hop has a strict payload limit, set the maximum explicitly.
The next hop also has a limit. The Collector’s OTLP gRPC receiver, for example, accepts messages up to 4 MiB by default. A batch of thousands of spans with large attributes can cross that line, and the sender then receives a rejection. Estimate your average item size, multiply by the batch, and leave a safe margin.
Two Very Different Behaviors From One Setting
With the default 8,192 and 200 milliseconds, a busy and a quiet Collector behave like two different machines.
- Busy case. At 100,000 spans per second, 8,192 spans arrive in about 82 milliseconds. The size trigger fires long before the 200-millisecond timer, so batches are full and the processor sends about 12 per second.
- Quiet case. At 500 spans per second, filling 8,192 would take more than 16 seconds. The timer fires every 200 milliseconds and releases batches of only about 100 spans. Efficiency is far lower than the configuration suggests.
The lesson is that in quiet periods the timeout decides batch size, and in busy periods the size setting does. Tune both, and watch which trigger is actually firing, a topic covered later under monitoring.
Where It Sits in the Pipeline
Order matters. A healthy trace pipeline usually places components in this order: first the memory limiter, then any processors that add attributes, filter or sample data, and the batch processor last before the exporter. Batching late means you never waste effort grouping items that an earlier step would have thrown away. Putting the memory limiter first means it can refuse new data before the Collector’s memory fills.
The Collector community has been moving batching into the exporter’s own queue, so newer versions can group data right where it is sent. The idea stays the same, but setting names and defaults change between releases. Always check the documentation for the exact version you run.
Batching Per Tenant
A software-as-a-service platform may send data for many customers through one Collector. If the batch processor mixed everyone together, one batch could carry data for ten customers, and the exporter could not attach the right credentials or route to the right destination. Setting metadata_keys to a header such as a tenant identifier makes the processor keep a separate batch per value. The receiver must be told to keep request metadata, and the number of groups must stay bounded, because each group holds its own batch in memory.
GExporter Queues, Retries and Persistence
After a batch is ready, it still has to reach the backend, and the backend is never perfectly reliable.
The Sending Queue
Most Collector exporters come with a built-in sending queue. Think of it as a loading dock. Finished batches are placed on the dock, and a group of workers called consumers pick them up and carry them to the backend. Three settings define the dock: whether the queue is enabled, how many consumers work in parallel (default 10), and how many items the dock can hold (the size).
Unlike the SDK, which sends one batch at a time, the Collector’s exporter sends in parallel. Suppose each request to the backend takes 250 milliseconds and each batch holds 8,192 spans. One consumer sends four batches per second, about 32,800 spans. Ten consumers raise the ceiling to roughly 327,000 spans per second. If a gateway receives more than that, the queue starts to fill.
flowchart LR A["Batch processor
produces batches"] --> B{"Is the sending
queue full?"} B -->|"Yes"| C["Reject the batch
error goes back upstream"] B -->|"No"| D["Sending queue"] D --> E["Consumers send in parallel"] E --> F{"Did the backend
accept it?"} F -->|"Yes"| G["Done"] F -->|"Temporary error"| H["Wait with growing delay
then retry"] H --> E F -->|"Permanent error"| I["Drop the batch and count it"]
Do the Memory Math Before You Raise the Size
A larger queue absorbs longer backend outages, which feels like a pure benefit. The cost is memory. In many versions the queue size counts batches, not individual items. With a size of 1,000 batches of 8,192 spans each, a completely full queue holds more than eight million spans. Even at one kilobyte per span in memory, that is about eight gigabytes. A Collector sized for two gigabytes will be killed long before the queue reaches that point.
So the real design question is not “how big can the queue be” but “how many seconds of backend outage do I want to survive, and how much memory does that need”. Multiply your steady ingest rate by the outage duration, convert to memory using a measured item size, and add headroom.
Retries With Growing Gaps
When the backend answers with a temporary error, such as “unavailable” or “too many requests”, the exporter does not give up. It waits, tries again, and waits longer each time. This pattern is called exponential backoff. Typical defaults start at five seconds, grow toward a maximum interval of 30 seconds, and stop retrying after a total of about five minutes. A random jitter is added so that hundreds of Collectors do not all retry at the same instant.
Permanent errors, for example data the backend considers invalid, are not retried because retrying cannot change the answer. Those batches are dropped and counted.
If you knock on a door and nobody answers, you do not hammer on it every second. You wait a minute, knock again, then wait longer. If the neighbor tells you the person moved away, you stop knocking altogether. Backoff is polite knocking, and permanent errors are the “moved away” message.
Persistent Queues: Surviving a Restart
An in-memory queue disappears when the Collector restarts, whether the restart is planned or the result of a crash. A persistent queue stores waiting batches on disk through a storage extension, so they survive. The cost is disk speed and some extra delay for each batch. Persistence is worth it when the data cannot be regenerated and restarts are common, for example gateways running on frequently replaced machines. It is not worth it for data that is cheap and plentiful, such as high-volume debug logs.
Pros of a persistent queue
- Data survives Collector restarts and crashes
- Queue can be much larger than memory allows
- Smoother rolling upgrades of gateways
Cons of a persistent queue
- Disk writes add latency and wear
- Needs fast, reliable disk and capacity planning
- Corrupt or stale files need operational care
HMemory Limiter and Backpressure
When a pipeline cannot keep up, something must give. The goal is to choose what gives, instead of letting the process choose for you by crashing.
What Backpressure Means
Backpressure is the signal a slow consumer sends to its producers to say “slow down”. Water in a pipe shows the idea: if the outlet is blocked, pressure builds and travels backward through the pipe. In a telemetry pipeline, the backend slows down, exporter queues fill, the batch processor cannot hand over batches, the receiver cannot accept new data, and finally the sending SDK finds its requests rejected or delayed.
Backpressure is healthy when it is controlled. It becomes dangerous when nothing stops the buffers in the middle from growing without limit.
The Memory Limiter Processor
The Collector provides a memory limiter processor as a safety valve. It checks the process’s memory on a regular schedule, set by check_interval, commonly one second. You give it a limit, either as a fixed number of mebibytes or as a percentage of available memory, plus a spike limit that estimates how much memory can be added between two checks.
The limiter has two thresholds. The soft limit is the main limit minus the spike limit. When usage rises above it, the processor starts refusing new data, returning an error to the receiver so the sender knows to retry later. If usage keeps climbing past the hard limit, the processor also forces the runtime to free memory as aggressively as it can.
A nightclub doorman counts people in and out. When the room reaches a safe number, he stops letting new guests in and tells them to try again in a few minutes. The people queueing outside are inconvenienced, but the building does not collapse. Without the doorman, the room fills until the fire marshal shuts everything down.
What people do
They run a gateway Collector with a large exporter queue and no memory limiter, assuming that the container’s memory limit is protection enough.
Why it hurts
When the backend slows down, the queue grows until the container is killed for using too much memory. Everything in memory, including the queued data, is lost at once, and the restart makes things worse because upstream senders now retry all together.
Do this instead
Put the memory limiter first in every pipeline, set its limit comfortably below the container limit to leave room for the runtime, and size queues so that a full queue fits inside the limiter’s budget.
What the Sender Sees
When the limiter refuses data, the sending SDK’s exporter receives an error. Standard OTLP exporters treat certain errors as retryable and try again with backoff. While one export is being retried, the processor’s single worker is occupied. The SDK queue fills behind it and eventually drops the newest items.
This sounds bad, but it is the intended design. Losing a few spans at the edge, where they are cheap and the application is unharmed, is far better than losing a whole Collector and every sender’s in-flight data. Pressure is pushed out to the edges, where it can do the least damage.
Scaling the Gateway Correctly
If the limiter is refusing data regularly, the gateway needs more capacity, and adding Collectors is the natural answer. Choose the signal for autoscaling carefully. Memory is a poor trigger because the limiter holds it near the ceiling by design, so a healthy but busy Collector already looks full. CPU usage and queue utilization react earlier and describe the real bottleneck.
Alert on the refusal counters, not only on crashes. A Collector that refuses data for a few seconds during a spike is doing its job. A Collector that refuses data for hours needs more capacity or less incoming volume.
ISizing Batches: The Math Behind the Numbers
Good tuning is arithmetic with a few measured inputs. Once you know four numbers, most settings follow.
The Four Inputs
- Arrival rate. How many items per second the process or Collector receives, at peak and on average.
- Export latency. How long one send takes from start to acknowledgment, including network time and backend processing.
- Stall tolerance. How many seconds of backend trouble you want to survive without dropping data.
- Item size. The average memory an item uses while waiting, and the average bytes it takes on the wire. Measure it instead of guessing.
Little’s Law in Plain Words
A simple rule from queueing theory, known as Little’s Law, says that the average number of items inside a system equals the arrival rate multiplied by the average time each item spends there. If 100 customers arrive per hour and each stays 30 minutes, about 50 customers are in the shop at any moment.
A coffee shop receives two orders per second and each order waits three seconds before the barista picks it up. On average, six orders are sitting in the line at any moment. If the barista slows down and the wait doubles, the line doubles too. The queue in your SDK or Collector behaves the same way: longer waits mean more items parked in memory.
Applied to telemetry, the rule gives a direct way to size things. Items waiting in the queue equal the arrival rate times the seconds they wait. When the backend stalls, the wait grows by the stall length, so the queue must hold the arrival rate times the stall time, plus normal waiting.
Worked Example One: A Busy Service Instance
One instance of an order service creates 4,000 spans per second at peak. The agent Collector on its machine answers in about 100 milliseconds, and the team wants to survive a five-second Collector hiccup.
Check the throughput ceiling
Batch size divided by latency: 512 divided by 0.1 seconds is 5,120 spans per second. That is only about 28 percent above the arrival rate, which is thin. Choose a batch of 1,024, giving a ceiling near 10,240.
Size the queue for the stall
4,000 spans per second for five seconds is 20,000 spans. Add a 50 percent safety margin to reach 30,000, then round up to a power of two: 32,768.
Convert to memory
Assuming roughly two kilobytes per waiting span, a full queue holds about 64 megabytes. Check that the service has that much spare memory, and replace the assumption with a measured number later.
Choose the delay
At 4,000 spans per second a batch of 1,024 fills in about a quarter of a second, so the delay rarely matters at peak. Keep it at two seconds so quiet hours still deliver data promptly.
Worked Example Two: A Gateway Collector
A fleet produces 200,000 spans per second in total, spread over 10 gateway Collectors, so each handles 20,000 per second. The team uses the defaults: a size trigger of 8,192 and a timeout of 200 milliseconds.
Filling 8,192 spans at 20,000 per second takes about 410 milliseconds, which is longer than the 200-millisecond timer. The timer wins every time, and each batch holds only about 4,000 spans, half of what the setting suggests. The gateway sends five batches per second where it could have sent two or three.
There are two fixes. The team can raise the timeout to 500 milliseconds so the size trigger wins and batches are full, at the cost of up to 300 extra milliseconds of delay. Or it can run fewer, larger gateways so each receives more traffic. Neither is automatically right; the choice depends on how much freshness the business needs.
| Setting | Rule of thumb | Why |
|---|---|---|
| SDK batch size | At least two times arrival rate multiplied by export latency | Keeps the serial exporter ahead of arrivals |
| SDK queue size | Arrival rate times stall seconds times 1.5 | Absorbs a backend hiccup without drops |
| Collector size trigger | Close to what the backend ingests efficiently | Keeps requests useful but below payload limits |
| Collector timeout | Longer than arrival rate needs to fill the batch, shorter than your freshness budget | Lets the size trigger win when busy |
| Exporter queue size | Stall seconds times batches per second | Survives outages within the memory budget |
Measure item size from real traffic. Divide the bytes the exporter sends by the number of items in those requests, and sample memory use with and without a full queue. Planning from measured numbers beats planning from vendor examples.
JFreshness, Efficiency and Memory: Choosing a Profile
Every setting moves you along three axes. You cannot improve all three at once, so you choose which one matters most.
The Latency Budget
Data takes time to appear in a dashboard, and the delay is the sum of several waits. Understanding where the time goes tells you which knob is worth turning.
SDK wait
Up to the scheduled delay, 5 seconds by default for spans, if traffic is too low to fill a batch.
Agent wait
Up to the agent’s batch timeout, often 200 milliseconds, plus a short network hop.
Gateway wait
Up to the gateway’s batch timeout, then time in the exporter queue if the backend is busy.
Backend wait
Many backends buffer and index before data becomes searchable, often seconds and sometimes longer.
Adding the worst cases, a quiet service can see a span take 5 plus 0.2 plus 0.2 plus perhaps 10 seconds, so around 15 seconds. The biggest and cheapest piece to shorten is usually the SDK delay for low-traffic services. For high-traffic services the delay seldom fires, so shortening it changes little, and the backend wait dominates.
Three Starting Profiles
The table below shows illustrative starting points, not laws. Treat them as hypotheses to test against your own traffic.
| Setting | Low latency | Balanced | High throughput |
|---|---|---|---|
| SDK batch size | 512 | 1,024 | 2,048 |
| SDK delay | 1 second | 2 seconds | 5 seconds |
| SDK queue size | 4,096 | 16,384 | 32,768 or more |
| Collector size trigger | 2,048 | 8,192 | 8,192 to 16,384 |
| Collector timeout | 100 ms | 200 to 500 ms | 1 second |
| Metric interval | 15 seconds | 30 to 60 seconds | 60 seconds |
| Best for | Live debugging, tight alerting | Most production services | Cost-sensitive, very high volume |
Pros of larger batches
- Fewer requests and lower CPU
- Better compression ratios
- Steadier load on the backend
- Lower per-item cost on metered services
Cons of larger batches
- Data appears later
- More items lost if a process dies mid-wait
- Bigger memory footprint
- Higher risk of exceeding payload limits
Choosing a profile is like choosing how a restaurant kitchen sends out food. Serving each dish the moment it is ready gets food hot but needs many trips. Waiting to load a big tray saves trips but cools the food. A good kitchen sends sushi fast and soup in trays, matching the method to what is carried.
Different Signals, Different Profiles
You do not have to use one profile for everything. Metrics that drive alerts can use a short interval while detailed traces use larger batches. Debug logs can be delayed and compressed heavily, while error logs go through a separate low-latency pipeline. The Collector allows several pipelines in one process, each with its own batch settings, so freshness is spent only where it pays off.
KScaling Out: Agents, Gateways and Load Balancing
At some size one Collector is not enough, and the layout of Collectors becomes part of your batching design.
Why Use Two Tiers?
An agent Collector runs next to the applications, often one per server or as a helper inside each pod. Applications send to it over a very short network hop, so SDK exports finish quickly and the SDK queue stays nearly empty. The agent adds host details, performs the first batching and retry, and shields applications from backend trouble.
A gateway pool sits between all agents and the backend. It is the place for expensive shared work: tail sampling, heavy filtering, routing by customer, and holding credentials for the backend so that applications never see them.
flowchart TD A1["Service pods on node 1"] --> G1["Agent Collector node 1"] A2["Service pods on node 2"] --> G2["Agent Collector node 2"] A3["Service pods on node N"] --> G3["Agent Collector node N"] G1 --> LB["Layer 7 load balancer
or client-side balancing"] G2 --> LB G3 --> LB LB --> GW1["Gateway Collector 1"] LB --> GW2["Gateway Collector 2"] LB --> GW3["Gateway Collector M"] GW1 --> BE["Backend"] GW2 --> BE GW3 --> BE
Mail works the same way. Neighbourhood post offices collect letters from local mailboxes and bundle them. Regional hubs combine bundles from many offices and send large shipments to the destination. Each layer reduces the number of trips the next layer must handle.
How Agent Settings Shape Gateway Load
Agents multiply. A cluster with 2,000 nodes has 2,000 agents, and each agent sends requests to the gateway pool. If traffic per node is modest, the agent’s 200-millisecond timer releases a batch five times per second, which means 10,000 requests per second arrive at the gateways even though each request is small. Raising the agent timeout to one second cuts that to 2,000 requests per second, and each request carries more data. The gateways do less per-request work, and the price is up to 800 more milliseconds of delay.
The Long-Lived Connection Trap
gRPC keeps one connection open and reuses it for many requests. That is efficient, but it interacts badly with simple load balancers that spread connections instead of requests. Each agent connects once, gets assigned to one gateway, and stays there for hours. When you add gateways during a traffic spike, the new ones receive almost nothing because every existing connection is already attached to an old one. Some gateways run hot while others sit idle.
There are three common remedies. Use a load balancer that understands HTTP/2 and spreads individual requests. Let clients resolve several gateway addresses and rotate among them. Or set a maximum connection age on the gateway’s receiver, so it politely closes connections after a while and clients reconnect to whichever gateway is available. Setting a maximum age also helps rolling deployments finish smoothly.
If gateway CPU is uneven after scaling out, suspect connection pinning before you suspect the batch settings. Compare per-gateway request counts; if a few gateways carry most of them, the balancing layer is the problem.
Sidecars and Direct Export
Two other layouts exist. A sidecar runs a tiny Collector inside every pod, giving each application its own private batching layer at the price of many small Collectors. Direct export skips Collectors and sends from the SDK straight to the backend. That is simplest for small systems, but every retry, credential and backend outage then lands inside the application process, so most large teams avoid it.
LSampling, Routing and Other Things That Hold Data
Some Collector components keep data for a while before releasing it. Each one adds to the batching story.
Head Sampling and Tail Sampling
Sampling means keeping only a portion of the traces. Head sampling decides when a trace begins, for example “keep one in twenty”. It is cheap and happens before data reaches any queue, which makes it the first line of defence against volume. Its weakness is that it decides blindly, and an interesting slow request may be discarded by chance.
Tail sampling decides after the trace is complete, so it can say “keep every trace that contains an error or took longer than two seconds”. To do this the Collector’s tail sampling processor must hold every span of every in-progress trace until a decision wait, 30 seconds by default, has passed.
The Memory Cost of Waiting
Holding data for a decision is just a very long wait inside a queue, so Little’s Law applies again. A gateway receiving 20,000 spans per second and waiting 30 seconds holds about 600,000 spans at any moment. At a kilobyte each, that is roughly 600 megabytes before counting batches, exporter queues and runtime overhead. The processor also limits the number of traces it tracks, so traffic bursts can evict traces before a decision is made.
A newsroom editor decides which stories to print only after seeing every piece of a story. Reporters’ notes pile up on the desk until the story is complete. The longer the editor waits for late pieces, the bigger the pile. A tail sampling Collector is that desk.
All Spans of a Trace Must Meet
Tail sampling only works if every span of a trace reaches the same Collector. Spans from one trace come from many services and machines, so a plain round-robin gateway pool would scatter them. The standard answer is a first tier whose exporter uses a load-balancing exporter. It looks at the trace identifier of each span and sends all spans with the same identifier to the same second-tier Collector. The second tier runs the tail sampling.
This has a batching side effect. The load-balancing exporter splits each incoming batch by destination. A batch of 8,192 spans spread over 10 second-tier Collectors becomes ten requests of about 800 spans. The more destinations you add, the smaller the pieces become, so first-tier batch sizes should be larger than you would otherwise choose, or you accept smaller requests between the tiers, where the network is usually cheap.
Adding or removing second-tier Collectors changes how trace identifiers map to destinations. Traces in flight can be split across two Collectors, and both may make the wrong decision. Scale this tier gradually, avoid constant autoscaling, and prefer warm, stable capacity over rapid adjustments.
Components That Create Their Own Rhythm
Other components also hold data. A span-to-metrics connector turns traces into counts and durations and flushes them on its own interval, which means it adds its own freshness delay and its own memory use. Components that aggregate, deduplicate or group telemetry follow the same pattern. Each time you add one, ask three questions: how long does it hold data, how much memory does that hold cost, and where should the batch processor sit relative to it.
| Component | Holds data for | Memory effect | Placement hint |
|---|---|---|---|
| SDK batch processor | Up to the scheduled delay | Queue size times item size | Inside every application |
| Collector batch processor | Up to the batch timeout | One batch per group | Late, just before exporters |
| Tail sampling processor | Decision wait, about 30 seconds | Rate times wait times item size | Second tier, after routing by trace |
| Exporter sending queue | Until sent or dropped | Queue size times batch size | Bounded by the memory limiter |
| Span-to-metrics connector | One flush interval | Series count | Before sampling so counts stay accurate |
MCompression, Payload Size and Protocol Choices
A batch is not only a count of items. It is also a number of bytes, and bytes are what networks, receivers and bills care about.
Why Bigger Batches Compress Better
Compression works by finding repeated patterns and replacing them with short references. Telemetry is very repetitive. Span names, attribute keys, service names and host details appear again and again. OTLP also groups spans under shared resource and scope descriptions, so a batch of 1,000 spans from one service stores the long list of resource attributes once, not 1,000 times. A batch of one span cannot share anything.
Packing for a trip with a vacuum bag works best when you pack many similar items together. One shirt in a bag saves nothing. Twenty shirts in a bag shrink to a fraction of their size. Batches of similar telemetry are the shirts, and compression is the vacuum cleaner.
Choosing a Protocol and a Compressor
| Option | Strengths | Watch out for |
|---|---|---|
| OTLP over gRPC | Binary and compact, long-lived connections, streaming-friendly, widely supported | Needs HTTP/2-aware load balancing, connection pinning, default message size limits |
| OTLP over HTTP with Protobuf | Works through ordinary proxies and load balancers, easy to debug | A new request per batch, slightly more overhead per call |
| OTLP over HTTP with JSON | Human-readable, handy for testing | Much bulkier and slower to encode, rarely right for high volume |
Compression choices carry a trade-off between CPU and bandwidth. Many SDKs send uncompressed data unless told otherwise, and an environment variable switches on a compressor. The Collector supports several, including gzip and zstd. Zstd usually reaches a similar or better ratio than gzip for less CPU, but both sides must support it. In cloud environments, traffic that crosses availability zones or leaves a network is often billed, so compression between agents and gateways can pay for itself in money as well as bandwidth.
Compression also changes where the bottleneck sits. At very high rates, compressing on the sender uses CPU on the application host, which may be the resource you can least afford to spend. Teams often keep the application-to-agent hop uncompressed, because it is local and cheap, and compress on the longer hops between agents, gateways and the backend.
Staying Under Payload Limits
Receivers and backends refuse messages above a certain size. Suppose the average span is 600 bytes. A batch of 8,192 spans is then about 4.9 million bytes, which exceeds a 4 MiB limit of roughly 4.2 million bytes. Larger attributes, such as long stack traces or database statements, make the problem worse. The result is a rejected request that cannot be fixed by retrying, because the batch is simply too big.
Defensive setups use three layers. They estimate average and worst-case item size. They set a maximum batch size on the Collector so oversized batches are split automatically. And they limit attribute length at the SDK, so a single misbehaving log line cannot create a monster span. To be safe, treat message size limits as applying to uncompressed data unless you have verified otherwise.
A sudden jump in batch bytes with no jump in item counts usually means a code change added big attributes. Watching bytes per item, not only items per second, catches this early.
NWatching the Pipeline Itself
You cannot tune what you cannot see. The batching layers expose signals that tell you what to change next.
The Pipeline Needs Its Own Dashboard
The Collector reports about itself, using the same kind of metrics it handles for others. Traditionally these are served in the Prometheus text format on port 8888, and newer versions let you configure the destination in the Collector’s own settings. Metric names carry a prefix and can differ slightly between versions, so check the names exposed by your own build before you write alerts.
A factory manager does not stare at the boxes. She watches the conveyor belt: how fast it moves, how full the holding area is, how many items were rejected, and how long a box takes to travel from one end to the other. Telemetry pipelines need the same four gauges.
The Signals Worth Watching
| What you see | What it usually means | First thing to try |
|---|---|---|
| Many timeout-triggered sends, few size-triggered sends | Batches leave small because traffic is too light to fill them | Raise the timeout, merge traffic onto fewer Collectors, or accept small batches |
| Batch size histogram clustered at the maximum | Traffic is high and every batch fills | Healthy; consider a larger batch only if backend latency is high |
| Exporter queue near its capacity | The backend or network cannot keep up | Check backend health, add consumers, add Collectors |
| Enqueue failures rising | Queue is full and data is being turned away | Find the slow stage before enlarging the queue |
| Send failures rising | Backend is rejecting or unreachable | Check credentials, limits and backend status |
| Refused data at receivers or processors | The memory limiter is protecting the process | Scale out or reduce incoming volume |
| Memory climbing steadily | Queues or tail sampling hold more data than planned | Revisit the queue size and decision wait |
The most useful single number is queue utilization: the current queue length divided by its capacity. A queue that stays below a modest fraction of capacity has plenty of room. A queue that sits above roughly 70 percent for several minutes is already in trouble, because a small additional stall will overflow it.
Equally useful is the ratio of timeout-triggered sends to size-triggered sends. It reveals which rule is controlling your batch sizes, and so tells you whether tuning the timeout or the size will help.
The SDK Side Is Quieter
Application SDKs historically say little about their own queues. Many only write a warning line when they drop items, and some do not even do that by default. Newer SDK releases are adding built-in self-metrics for queue size and processed counts, but support varies by language and version. Until your SDKs report them, two cheap tricks help. Watch the Collector’s received-item rate per service, which falls if the SDK is dropping. And compare the number of requests an application handled with the number of root spans that reached the backend.
Measure Freshness With a Canary
Internal counters tell you whether items move. They do not tell you how long the whole journey takes. A canary is a small synthetic producer that emits a tagged span or log every few seconds. A checker queries the backend for it and measures the gap between creation and visibility. That gap is your real end-to-end freshness, including every wait described in this guide, and it is the number users actually feel.
OWhen Things Go Wrong: Failure Scenarios and Recovery
Tuning is tested during outages. Walk through what each layer does when the backend disappears.
A Backend Outage, Layer by Layer
sequenceDiagram
participant App as Application SDK
participant Agent as Agent Collector
participant GW as Gateway Collector
participant BE as Backend
App->>Agent: Batch of spans
Agent->>GW: Batch of spans
GW->>BE: Export request
BE-->>GW: Error, service unavailable
GW->>GW: Queue batch and retry with backoff
Note over GW: Queue fills during a long outage
GW-->>Agent: Refuse new data
Agent->>Agent: Queue batch and retry
Agent-->>App: Slow answers or refusals
App->>App: SDK queue fills and new spans are dropped
In the first seconds the gateway’s retries and queue hide the problem. Nothing is lost and nobody notices. As the outage continues, the gateway queue fills and the memory limiter starts refusing data. The agents now see errors, store batches in their own queues and retry. If the outage lasts longer still, agent queues fill and the SDKs start receiving slow answers or refusals. Finally the SDK queues fill, and the newest spans are dropped.
Each layer is a shock absorber, and the total outage you can survive is roughly the sum of the buffer times of all layers. This is why each buffer was sized from stall math earlier: you decide in advance how long you can ride out a failure and where the first drops should occur.
Common Failure Cases
| Scenario | What happens | Protection |
|---|---|---|
| Short backend blip | Retries succeed within seconds | Exporter retry and queue |
| Long backend outage | Buffers fill; edge drops new data | Sized queues, memory limiter, persistent queue for key tiers |
| Collector killed for memory | In-memory queue and in-flight batches vanish | Memory limiter below container limit, bounded queues |
| Rolling deployment | Pods stop while holding queued items | Graceful shutdown with enough time to flush |
| Short-lived job or serverless function | Process exits before the timer fires | Explicit flush before exit |
| Oversized batch | Receiver rejects it; retrying cannot help | Maximum batch size, attribute length limits |
| Network split between tiers | Sender cannot reach receiver | Retries, local queues, multiple gateway addresses |
The Recovery Surge
When the backend returns, every queue tries to drain at once. Gateways have minutes of backlog and ten consumers each, so the backend may receive many times its normal traffic and fall over again. This is a retry storm. Backoff with random jitter spreads the first wave. Limiting consumers, using a backend that can absorb bursts, or draining queues from the oldest data first at a controlled pace all help. Some teams deliberately accept dropping old data after an outage, because fresh data is more valuable than stale data and recovery stays calm.
Delivery Is Best Effort
A telemetry pipeline is designed to degrade gracefully, not to promise perfect delivery. Retries can cause duplicates, because a request may succeed on the backend even though the reply was lost. OTLP also allows partial success, where the backend accepts some items and reports the rest as rejected. Backends should tolerate duplicates, and teams should decide in advance which signals are allowed to lose data and which need stronger guarantees such as persistent queues.
Containers usually get a limited time between a stop request and a forced kill. If the SDK or Collector needs ten seconds to flush but the grace period is five, the last batch is lost every deployment. Make the grace period comfortably longer than the longest flush.
PBest Practices and Anti-Patterns
Teams that run telemetry at scale tend to arrive at the same habits. Here they are, along with the mistakes that teach them.
Habits That Work
Export locally
Send from applications to a nearby agent. Short hops keep SDK exports fast and queues empty.
Limiter first, batch last
Protect memory before anything else, then filter and sample, then batch just before export.
Cap the batch
Set a maximum batch size and attribute length so oversized requests never reach a limit.
Size from math
Derive queues from arrival rate, stall tolerance and measured item size, then check the memory total.
Sample early
Reduce data at the source with head sampling, and add tail sampling only where its memory cost is justified.
Flush on exit
Call shutdown or flush in every program and give containers enough grace time to finish.
Watch the pipeline
Dashboards and alerts for drops, queue use and freshness, plus a canary that measures the whole path.
Version the settings
Keep batching configuration in source control with the numbers behind each choice, and change one thing at a time.
Mistakes That Teach
What people do
They copy a configuration from a blog post or another team and apply it unchanged to every service.
Why it hurts
Arrival rate, latency and item size differ widely between services. A queue that is generous for one service overflows for another, and a timeout that suits a busy gateway produces tiny batches on a quiet one.
Do this instead
Treat shared examples as hypotheses. Compute your own numbers, apply them to a canary service first, and compare the pipeline dashboard before and after.
What people do
They use the simple processor in production because “we want every span right away”.
Why it hurts
Every request now waits on a network call, so a slow Collector slows the application itself. Throughput collapses exactly when the system is already under stress.
Do this instead
Use the batch processor everywhere and lower its delay if freshness is the goal. Reserve the simple processor for local debugging.
What people do
They respond to every drop by doubling the queue size.
Why it hurts
If the arrival rate is higher than the drain rate, a bigger queue only postpones the overflow while consuming more memory. If the drain rate is fine and the drops were a short stall, a moderate queue was already enough.
Do this instead
Find out whether the problem is throughput, a stall or a burst. Fix the drain rate for throughput, size the queue for stalls, and smooth bursts with sampling or filtering.
QSecurity and Privacy in a Batched Pipeline
Telemetry often contains more sensitive information than people expect, and batching changes where that information sits and for how long.
What Is Inside Your Batches
A span or log line is built from attributes that developers add. Over time those attributes can pick up customer email addresses, account numbers, request bodies, database statements and internal host names. Because batching holds data in memory and sometimes on disk before it is sent, every waiting room is also a place where sensitive data can be read by anyone who gets access to the machine, a memory dump or a leftover file.
A mail truck carries hundreds of letters at once. If someone breaks into the truck, they get everyone’s mail, not just one letter. Batches work the same way: grouping makes delivery efficient, and it also concentrates the damage if a buffer is exposed.
Protect Data Before It Waits
The safest data is data that was never stored. Filtering and redaction processors that remove or mask sensitive attributes should run before the batch processor and before any persistent queue. In the Collector this means placing them early in the processor chain, right after the memory limiter. Redacting later means the unprotected values have already been written into queues and temporary files.
Many teams also limit attribute lengths and block known-sensitive keys at the SDK. That keeps risky values out of the pipeline entirely and reduces payload size at the same time, which is a rare case where the safer choice is also the cheaper one.
Encrypt the Hops and Guard the Disk
Every network hop in Figure 1 should use encryption in transit, usually TLS, and should prove who is calling, either with client certificates or with tokens passed in request headers. Batching makes this cheaper than it sounds. A TLS handshake is costly, but a long-lived connection carrying large batches pays that cost once and shares it across thousands of items.
A persistent queue writes batches to disk. Those files contain the data exactly as it would be sent, so they need the same care as a database. Use encrypted volumes, restrict file permissions to the Collector’s own account, and clear or expire old queue files when a machine is retired.
Teams add a persistent queue for reliability and forget that it also creates a plain copy of production data on disk. Treat the queue directory as sensitive storage from day one, and include it in backup, retention and access reviews.
Credentials Belong at the Gateway
The backend usually requires an API key or token. If every application holds it, a leak from any one service exposes the key, and rotating it means redeploying everything. A gateway Collector can hold the credential and attach it when it exports, so applications only need permission to talk to the nearby agent. This also lets you apply different keys per tenant when batches are grouped by customer.
Defending the Pipeline Itself
A pipeline that accepts data from many senders can be overwhelmed by accident or on purpose. A buggy release might create millions of unique metric series, or a hostile client could send enormous requests. The defences are the ones already covered in this guide, now seen as security tools: maximum request size on receivers, a bounded batch size, a memory limiter that refuses data when full, per-tenant grouping limits, and limits on attribute length and series count. Each setting protects not only performance but also the availability of the whole monitoring system.
| Risk | Where it lives | Batching-aware defence |
|---|---|---|
| Sensitive attributes in memory | SDK and Collector queues | Redact early, before batching |
| Plain copies on disk | Persistent queue directory | Encrypted volume, strict permissions |
| Stolen backend key | Application configuration | Keep credentials on the gateway only |
| Oversized or endless requests | Receivers | Request size limits, memory limiter |
| Cardinality explosion | Metric pipelines | Series limits and attribute allow-lists |
RCost Thinking: What Batching Saves and What It Spends
At scale, tuning is also budgeting. Every setting moves money between CPU, network, memory and backend charges.
Where the Money Goes
A telemetry system has four main cost areas. Compute pays for the CPU that encodes, compresses, batches and forwards data, both inside applications and in Collectors. Network pays for bytes moved, and in many clouds, traffic between zones or out of a network is billed per gigabyte. Memory and disk pay for queues and buffers. Backend ingestion and storage usually costs the most, and it is often priced by volume.
Batching mainly reduces compute and network, because it cuts per-request overhead and enables compression. It rarely changes backend volume, because the backend still stores every item it receives. That is why sampling and filtering, which reduce item counts, usually save more than any batch setting.
A Back-of-the-Envelope Example
Take a platform producing 100,000 spans per second with an average size of 600 bytes. That is 60 megabytes per second, which adds up to about 5.2 terabytes per day before compression. If compression shrinks the data to a fifth of its size, which is plausible for repetitive spans but must be measured on your own data, the network carries about one terabyte per day.
Raw volume
100,000 spans per second at 600 bytes is 60 MB per second, or about 5.2 TB per day.
After compression
With an assumed five-to-one ratio, hops that cross billed boundaries carry roughly 1 TB per day.
After sampling
Keeping ten percent of traces before export cuts the same hop to about 0.1 TB per day, and shrinks every queue and CPU bill with it.
The takeaway is the order of magnitude. Compression gives a large saving on the wire, sampling gives a larger saving everywhere, and batch size tuning refines what remains. If your first optimization is fiddling with a timeout, you may be working on the smallest lever.
Shipping costs depend on how many trucks you send, how heavy the cargo is, and how many items you ship at all. Filling trucks is useful, wrapping cargo tightly helps, but deciding not to ship items nobody needs saves the most.
The Cost of Being Too Safe
Reliability settings have prices too. A queue sized for a one-hour outage needs memory or disk that sits idle almost all the time. A persistent queue needs fast storage. Over-provisioned gateways run at low utilization to leave room for bursts. These choices are justified when the data is valuable, but they should be made deliberately and written down, because they are the first items people question when budgets tighten.
The Cost of Being Too Lean
The opposite mistake is also expensive. Dropping telemetry during an incident means engineers debug blind, and every extra hour of an outage costs far more than a few gigabytes of buffer. A fair way to decide is to ask what a lost hour of data costs the business, then compare that with the monthly cost of the buffers that would have saved it.
Cheap levers
- Head sampling of low-value routes
- Dropping debug logs at the source
- Enabling compression on long hops
- Capping attribute length
Expensive levers
- Very large persistent queues everywhere
- Tail sampling with long decision waits
- Very short metric intervals across a huge fleet
- High-cardinality attributes on metrics
SLanguage and Runtime Differences That Affect Batching
The specification defines the same settings for every language, but the runtime underneath changes how those settings behave.
Where the Background Worker Lives
A batch processor needs a background worker to wake up on timers and send data. How that worker is built depends on the language. In Java it is usually a dedicated daemon thread. In Go it is a goroutine, a very light unit of concurrent work. In Python it is a normal thread. In Node.js there are no extra threads for this job, so work is scheduled on the single event loop that also serves your requests. In each case the same four knobs apply, but the cost of the worker differs.
Some restaurants have a dedicated runner who carries plates to tables. In others, the cook also carries plates between cooking tasks. The menu and the customers are the same, but a big order slows the cook-and-runner kitchen far more than the kitchen with a separate runner.
Node.js: Watch the Event Loop
In Node.js, turning thousands of spans into bytes happens on the same thread that handles incoming requests. A very large batch can block the event loop for a noticeable time while it is encoded, which shows up as latency spikes in the application itself. Moderate batch sizes, sent more often, keep each pause short. If you see periodic latency spikes that line up with the export schedule, large batches are a prime suspect.
Python and Pre-Fork Servers
Many Python web servers start a parent process and then copy it into several worker processes, a technique called forking. Threads do not survive a fork. If the batch processor was started in the parent before forking, the copied workers inherit a queue with no background worker behind it. Spans pile up, nothing is sent, and the queue eventually fills and drops data silently. The usual remedy is to initialize telemetry inside each worker after the fork, using the server’s own hook for that moment.
If one server works perfectly in development but sends no data in production, check whether production uses a pre-fork model. This failure produces no error and is easy to mistake for a networking problem.
Java and Go: Shutdown Hooks and Contexts
Long-running Java and Go services need the flush to run when the process receives a stop signal. In Java a shutdown hook can close the tracer provider, and in Go the program should call the provider’s shutdown function with a deadline before exiting. Without it, the last few seconds of data, often the most interesting moments before a crash or deployment, are discarded.
Serverless and Short-Lived Runtimes
Serverless platforms freeze or destroy a function’s environment once the handler returns. Timers do not run while frozen, so a five-second delay may never expire. Functions need an explicit flush before returning, and they benefit from a very close Collector or platform extension so that the flush completes in milliseconds.
Do Not Assume Identical Defaults
The specification describes standard defaults, and most SDKs follow them, but implementations differ in detail: how drops are reported, whether exports can run concurrently, and which environment variables are honored. Before relying on a number from this guide in a particular language, confirm it in that SDK’s documentation and in a short test that fills the queue deliberately.
| Runtime | Worker | Main thing to watch |
|---|---|---|
| Java | Daemon thread | Shutdown hook to flush |
| Go | Goroutine | Shutdown with a deadline |
| Python | Thread | Pre-fork servers losing the worker |
| Node.js | Event loop timers | Large batches blocking requests |
| Serverless | Frozen between calls | Explicit flush on every invocation |
TAdaptive Tuning: Letting the System Respond to Pressure
Fixed numbers are chosen for an average day. Real traffic is not average, so mature teams add a few mechanisms that react while the system is running.
Static Versus Dynamic Settings
Everything covered so far is a static setting: you pick a number, deploy it, and it stays until someone changes it. That works when traffic is predictable. When traffic swings widely, a dynamic layer on top of the static settings gives you room to maneuver without redeploying anything. The dynamic layer never replaces the careful sizing from earlier chapters. It only decides what to sacrifice first when the system comes under pressure.
Some highway on-ramps have traffic lights that let only one car through at a time during rush hour. The light does not widen the road. It protects the highway by controlling how fast new cars join. Adaptive tuning is a set of ramp lights for telemetry: the pipeline keeps flowing because entry is managed.
Autoscaling the Gateway Tier
The most common dynamic mechanism is adding or removing gateway Collectors as load changes. It works best for stateless pipelines, where any Collector can handle any request. Scale on signals that rise before trouble starts, such as CPU use and exporter queue utilization, and scale out quickly but scale in slowly, so a brief lull does not remove capacity just before the next burst. Stateful tiers, such as tail sampling, need much gentler scaling because changing the number of Collectors reshuffles which Collector owns which trace.
Priority Lanes
Not all telemetry is equally valuable. Error traces, payment logs and health metrics matter more than debug messages and low-value routes. A Collector can route data into separate pipelines by attribute, giving the important lane its own small batch timeout, its own queue and its own export path. The bulk lane gets larger batches and a longer timeout. When the backend slows, the bulk lane fills first and drops first, while the priority lane keeps flowing.
Load Shedding
Load shedding means deliberately discarding the least valuable data once pressure passes a threshold, so the valuable data survives. A pipeline might start dropping debug-level logs when queue utilization passes a set level, or lower its sampling rate for noisy routes. Some tools support changing sampling rules remotely, so a central controller can tell thousands of processes to keep fewer traces without a redeploy. Management protocols such as OpAMP, designed for controlling fleets of telemetry agents, aim at the same goal for configuration in general.
Guard Rails for Adaptive Systems
Systems that adjust themselves can also oscillate. A controller that drops data when the queue is high and restores it when the queue is low can flip back and forth every few seconds, creating unstable behaviour that is harder to debug than the original problem. A few simple rules prevent this.
- Use separate thresholds for entering and leaving a mode. For example, start shedding at 80 percent queue use but stop only when it falls below 50 percent.
- Set hard floors and ceilings. A sampling rate should never be automatically lowered below a minimum that still lets you see errors.
- Make every change visible. Each automatic adjustment should appear in a metric and a log line, so that a human can tell why data volume changed.
- Practise manual override. On-call engineers need a documented switch that turns adaptive behaviour off during unusual incidents.
| Lever | Typical trigger | Main risk |
|---|---|---|
| Gateway autoscaling | CPU or queue utilization | Flapping, cold-start delay |
| Priority lanes | Fixed rules by attribute | Bad rules hide important data in the bulk lane |
| Load shedding | Queue use above a threshold | Losing data needed for a later investigation |
| Remote sampling changes | Cost or volume targets | Biased or inconsistent samples across services |
UReal-World Patterns at Scale
The same ideas show up differently depending on the shape of your traffic. These illustrative scenarios show how teams adapt the settings.
A flash sale on a large online store
Traffic can jump tenfold within minutes when a sale opens, the kind of peak any big retailer faces on a holiday. Teams size SDK queues for the peak rate, not the average, and pre-scale gateways before the event instead of waiting for autoscaling to react. They also turn up head sampling for low-value routes such as image requests, while keeping every checkout trace. The goal is that the drop point moves to the edge, where it is cheap, while payment traces stay complete.
A ride-hailing platform with hundreds of services
One ride request can touch dozens of services, so one user action may produce hundreds of spans. Uber built Jaeger for this reason, and its experience shaped how the industry thinks about sampling. At this scale a two-tier design is normal: agents batch locally, a first gateway tier routes spans by trace identifier, and a second tier makes tail sampling decisions so that errors and slow trips are always kept. Batch sizes on the routing tier are set larger so that splitting by destination still leaves reasonably sized requests.
Serverless functions that live for milliseconds
A function may run for less than one second and then be frozen or destroyed. A batch delay of five seconds means its spans might never be sent. The usual approach is a short delay or an explicit flush at the end of every invocation, plus a Collector or extension running right beside the function so the flush finishes in a few milliseconds. Here freshness and safety matter more than batch efficiency, because efficiency is gained elsewhere by the Collector layer.
A multi-tenant software-as-a-service product
Many customers share the same pipeline, and one noisy customer must not starve the rest. Teams group batches by tenant using request metadata, apply per-tenant limits, and sometimes dedicate separate pipelines to the largest customers. Each group holds its own batch in memory, so the number of tenant groups must be bounded, or memory grows with the customer count.
Edge sites with unreliable networks
Stores, factories and vehicles often lose connectivity for minutes or hours. A local Collector with a large, disk-backed queue stores data during the outage and sends it when the link returns, using strong compression to make the most of limited bandwidth. Freshness is deliberately sacrificed, and the backend must accept data that arrives long after it was created, with correct original timestamps.
VA Step-by-Step Tuning Playbook
When you are ready to tune a real system, work in this order. Each step removes uncertainty from the next.
Measure the baseline
Record arrival rate at peak and average, export latency, average item size in memory and on the wire, and the current drop rate. Without these, every later number is a guess.
Reduce volume first
Sampling, dropping noisy logs, limiting attribute length and controlling metric cardinality shrink every downstream buffer at once. This is the cheapest tuning you can do.
Fix the layout
Export from applications to a local agent, put gateways behind a balancing layer that spreads requests, and decide where tail sampling lives and how traces reach it.
Install the safety valve
Add the memory limiter first in every pipeline with limits below the container’s real limit, and confirm that queues fit within that budget.
Size the SDK
Check the serial throughput ceiling against peak arrival, then size the queue from the stall you want to survive. Verify memory use under a full queue.
Size the Collector
Choose the size trigger, timeout and maximum batch from backend limits and freshness needs. Confirm which trigger fires at typical and quiet traffic, then size the exporter queue and consumers.
Break it on purpose
In a test environment, stall or block the backend for the stall time you designed for and watch every layer. Confirm that nothing is dropped within tolerance, that the first drops happen at the intended place, and that recovery does not cause a second outage.
Roll out and revisit
Release to a small slice, compare dashboards, and widen gradually. Record the reasoning with the numbers, and review again when traffic, services or backends change.
Change one knob at a time and wait long enough to see its effect, including the quiet hours. A change that looks fine at noon may fail at 3 a.m., and two changes at once make it impossible to know which one helped.
WFrequently Asked Questions
Short answers to the questions that come up most often when teams start tuning.
XA Plain-Language Glossary
A quick reference for the terms used throughout this guide.
| Term | Plain meaning |
|---|---|
| Telemetry | Data a system produces about itself so people can understand what it is doing. |
| Signal | One kind of telemetry: traces, metrics or logs. |
| Trace and span | A trace is the journey of one request; each timed step along that journey is a span. |
| SDK | The OpenTelemetry library that runs inside your application and prepares telemetry for sending. |
| Processor | A component that handles each item on its way, for example by grouping it into a batch. |
| Exporter | The part that sends data out to another system. |
| OTLP | The standard format and protocol OpenTelemetry uses to move telemetry between components. |
| Collector | A separate program that receives, processes and forwards telemetry. |
| Receiver | The Collector part that accepts incoming data. |
| Pipeline | The chain of receiver, processors and exporter that data follows inside a Collector. |
| Agent and gateway | An agent is a Collector next to your applications; a gateway is a shared pool of Collectors in front of the backend. |
| Batch | A group of items sent together in one request. |
| Queue | A waiting line with a fixed maximum length that holds items until they are sent. |
| Backpressure | A signal travelling backwards from a slow consumer telling producers to slow down. |
| Backoff | Waiting longer after each failed attempt before trying again. |
| Persistent queue | A queue stored on disk so that waiting items survive a restart. |
| Memory limiter | A Collector safety valve that refuses new data when memory use gets too high. |
| Cardinality | The number of unique combinations of attribute values for a metric. |
| Head and tail sampling | Deciding what to keep at the start of a trace, or after it has finished. |
| Flush | Forcing waiting data to be sent immediately instead of waiting for the timer. |
| Canary | A small synthetic producer used to measure how long data takes to become visible. |
| Load shedding | Deliberately dropping the least valuable data so the most valuable data survives. |
YSummary and Key Takeaways
Batching in OpenTelemetry is a chain of waiting rooms. Tuning is deciding how large each room is and how long people may wait in it.
You started with a simple idea: sending items in groups is cheaper than sending them one by one. You then followed the idea through the SDK queue, the Collector batch processor, the exporter queue and the backend, and saw that every layer has its own size, its own timer and its own failure behavior. The arithmetic that ties them together is short, and measured numbers make it reliable.
Key Takeaways
- Batching happens in several layers. The SDK queue, the Collector batch processor, the exporter queue and the backend buffer each need settings that agree with each other.
- The SDK has a hidden speed limit. Its throughput ceiling is roughly batch size divided by export latency, so raise the batch size before you raise the queue size.
- Size queues from stall math. Queue length should equal arrival rate multiplied by the stall you want to survive, plus a margin, and the memory total must fit your budget.
- Timers decide batches when traffic is light. Check whether size or timeout triggers are firing, and tune the timeout, the layout or both.
- Protect memory first. Put the memory limiter at the start of every pipeline and batch at the end, so overload turns into controlled refusals instead of crashes.
- Cap payloads and watch the pipeline. Set a maximum batch size, limit attribute length, and monitor drops, queue use and end-to-end freshness with a canary.
- Reduce volume before tuning buffers. Sampling, filtering and cardinality control shrink every queue, and change one setting at a time with numbers written down.