Simple vs Batch Span Processor in OpenTelemetry
Two small components decide when your finished spans leave your program. One sends them the instant they end. The other collects them and sends them in groups. This guide explains how each works, what each costs, and how to choose without guessing.
Picture two ways of mailing letters. In the first, every time you finish writing a letter, you walk to the post office and hand it over. In the second, you drop each finished letter into a tray on your desk, and a helper takes the whole tray to the post office every few minutes. The first way is simple, and your letters are never sitting around on your desk. The second way saves a huge amount of walking, and your work is never interrupted by trips to the post office.
OpenTelemetry faces exactly this choice with spans. A span is a record of one timed step in your program, such as “load the user from the database”. When a span ends, something has to decide when to send it away. That something is called a span processor, and the two built-in choices are the Simple Span Processor and the Batch Span Processor.
The choice looks tiny, a single line in your setup. Yet it affects how fast your application responds, how much telemetry you lose when something goes wrong, how much memory you use, and whether your short-lived programs report anything at all. This guide assumes you know nothing. Every new word is explained when it first appears, with an everyday comparison.
1The Short Answer: One Page Before the Long Story
If you only have two minutes, this chapter gives you the whole difference. The rest of the guide explains why it is true and how to act on it.
The Simple Span Processor sends each span to the exporter immediately, one at a time, right when the span ends. The code that ended the span waits while the export happens. Nothing is stored in between. It is easy to understand and easy to debug, but it slows your application and makes many tiny network calls.
The Batch Span Processor puts each ended span into a waiting line in memory, called a queue. A background worker takes spans out of the queue in groups and sends each group together. Your application does almost no waiting. In exchange, spans sit in memory for a short time, and if the line gets too long, extra spans are thrown away.
| Question | Simple Span Processor | Batch Span Processor |
|---|---|---|
| When is a span sent? | At once, when it ends | Later, in a group |
| Who does the sending work? | The same thread that ended the span | A separate background worker |
| Does the application wait? | Yes, for every span | Almost never |
| Can spans be lost? | Rarely, but a slow exporter slows the app | Yes, if the queue fills or the program crashes before a send |
| Best for | Learning, tests, local debugging | Production services |
The Simple processor is a waiter who carries each dish to the table the moment it leaves the stove. The Batch processor is a waiter who loads a tray and delivers several dishes in one trip. For a quiet cafe with two tables, the first is fine. For a busy restaurant, the second keeps the kitchen moving.
A good rule of thumb: Simple while you are learning or testing on your own machine, Batch everywhere else. The exceptions are covered in later chapters.
2Why Spans Need a Way Out: The Problem Being Solved
Before comparing the two processors, it helps to see why a processor exists at all. The reason is that a span lives and dies inside your program, and nobody can read it there.
When your application handles a request, the OpenTelemetry tracer creates spans as work begins and finishes them as work ends. Each finished span holds a name, a start time, an end time, a status, and a set of small notes called attributes. All of this sits in your program’s memory. If your program stops, the memory is gone. And even while it runs, nobody outside the program can read that memory.
So the spans must be carried out to a place where people can read them. That place might be a tracing backend, an OpenTelemetry Collector, or just your terminal screen. The part that actually sends spans to such a place is called an exporter. The exporter knows how to speak a particular language, such as the OTLP format over the network, and how to reach a particular destination.
Here is the gap. The tracer knows when a span ends. The exporter knows how to send. Something has to sit between them and decide when and how many at a time. That in-between part is the span processor. Without it, the tracer would have to guess how to talk to exporters, and every exporter would have to guess when to send. Having a processor in the middle gives you one clear place to make timing decisions.
Why timing matters so much
Sending data over a network is slow compared with ordinary work inside a program. Reading from memory takes billionths of a second. Sending a message to another machine takes thousandths of a second or more, which is a million times longer. If your program stops to send every single span, those delays add up fast. A request that creates twenty spans could pay for twenty network trips.
Imagine a factory worker who stops the assembly line to walk each finished item to the shipping dock. The line would crawl. Factories use carts and loading bays so that workers keep working while a separate crew handles shipping. The span processor decides whether your program is the worker who walks, or the one who uses the cart.
The same idea exists for logs, where there are simple and batch log record processors. Metrics use a different mechanism, called a reader, but the question of when to send is similar. Learning span processors teaches you the pattern for all of them.
3Core Vocabulary You Need
A handful of words appear in every conversation about processors. Learn these once and everything else becomes easy to follow.
| Term | Plain meaning | Everyday picture |
|---|---|---|
| Span | A record of one timed step | One line on a stopwatch record |
| Trace | A group of spans for one request | The whole stopwatch record of a relay race |
| Tracer | The object your code uses to create spans | The person holding the stopwatch |
| Tracer provider | The central setup that owns tracers and processors | The race organizer |
| SDK | The OpenTelemetry library that does the real work inside your program | The engine under the hood |
| Span processor | Component that is told about every span start and end | The person at the finish line who collects results |
| Exporter | Component that sends spans out of the program | The courier |
| Queue | A waiting line held in memory | A tray of letters on a desk |
| Batch | A group of spans sent together | A box of letters |
| Flush | Send everything that is waiting right now | Emptying the tray |
| Shutdown | Flush and then stop for good | Closing the shop at night |
| Backend | The system that stores and shows traces | The archive and reading room |
Two small ideas hiding in the table
First, a span has two important moments: the moment it starts and the moment it ends. Processors are told about both. Only after a span ends is it complete and ready to export, because only then does it know its duration.
Second, a span processor and an exporter are different things that are easy to mix up. The processor decides when. The exporter decides how and where. You can pair either processor with almost any exporter. The Simple processor with a console exporter prints spans to your screen immediately. The Batch processor with an OTLP exporter sends groups of spans to a Collector.
Think of a delivery company. The dispatcher decides when trucks leave, which is the processor. The truck and driver know the route and carry the goods, which is the exporter. You can change dispatch rules without buying new trucks, and you can change trucks without rewriting the dispatch rules.
When reading documentation, notice which of these two parts a setting belongs to. A timeout on the exporter is about one send. A delay on the processor is about when sends happen.
4Where Processors Sit in the SDK Pipeline
A picture of the whole path makes the rest of the guide much easier. Follow a single span from birth to the backend.
flowchart LR
CODE["Your application code"] --> TR["Tracer creates a span"]
TR --> SP["Span starts and later ends"]
SP --> PROC["Span processor"]
PROC --> EXP["Span exporter"]
EXP --> NET["OTLP over the network"]
NET --> COL["Collector or backend"]
COL --> UI["Trace viewer"]
Walking through the picture
Your application code asks a tracer to start a span. The tracer creates the span object and tells every registered processor that a span has started. Your code does its work. When the work is done, your code ends the span. The span now has a duration, and the tracer tells every registered processor that the span has ended.
What each processor does at that moment is the whole difference we are studying. The Simple processor hands the span to its exporter right away. The Batch processor adds the span to its queue. Either way, the exporter eventually turns the span into a message, such as OTLP, and sends it across the network to a Collector or a backend. The backend stores it, and a trace viewer such as a web dashboard draws it for a human.
Notice that the processor is not in the way of span creation. Creating a span is very cheap and happens in memory. The processor only matters at the end, when the question becomes how to ship the finished span.
A newspaper has reporters, an editor’s desk, delivery trucks, and readers. Reporters finish stories at different times. The editor’s desk decides when to send stories to the printing press. The processor is that desk, and the rule it follows changes how quickly stories reach readers and how many trips the trucks must make.
You can register more than one processor on the same tracer provider. Each receives every span event. This allows combinations, such as one Batch processor sending to the network and one Simple processor printing to the console during debugging.
5The Span Processor Contract: Four Moments
Every span processor, built in or custom, promises to respond to the same four moments. Knowing them explains why both processors look so similar from the outside.
On start
Called when a span begins. The span is still open and can be changed, for example by adding an attribute. Both built-in export processors do nothing here.
On end
Called when a span finishes. The span is now read-only. This is where Simple and Batch behave completely differently.
Force flush
A request to send everything that is waiting, as soon as possible. Useful before a program pauses or exits.
Shutdown
A request to finish all work, send what remains, and release resources. Called once, at the end of the program’s life.
Why only the end matters for sending
A span can only be exported when it has ended, because a running span has no final duration and may still change. So the on-start moment is used for other jobs, such as stamping every span with a common attribute. The exporting processors, Simple and Batch, both ignore the on-start moment and act on the on-end moment.
Why flush and shutdown are in the contract
The Simple processor has nothing waiting, so flush and shutdown are quick for it. The Batch processor can have hundreds of spans in its queue, so these two moments are vital. They are the way you tell the Batch processor, “Stop waiting for the timer and send everything now.” Forgetting to call them is one of the most common causes of missing spans, a topic we return to in later chapters.
A shop has an opening time, a rush of sales, a request to bring out everything in the back room, and a closing routine. Even a tiny shop follows all four. A shop that never counts the back room at closing time loses stock without noticing.
Because spans are read-only after they end, a processor must never rely on changing an ended span. If you need to add data, do it on start, or inside your code before the span ends.
6The Simple Span Processor: How It Works
The Simple processor has almost no inner machinery. That is its charm, and also its limit.
When a span ends, the Simple processor does the following. First, it checks whether the span is sampled, meaning that the sampling rules decided this trace should be recorded. Unsampled spans are skipped. Second, it passes the span to the exporter by calling its export function with a list holding that one span. Third, it waits for the exporter to finish, in most implementations, so that spans are exported one at a time and in order. Then the call returns, and your application code continues.
sequenceDiagram
participant App as Application thread
participant SP as Simple processor
participant EX as Exporter
participant BE as Backend or Collector
App->>SP: Span ends
SP->>EX: Export one span
EX->>BE: Network call with one span
BE-->>EX: Acknowledge
EX-->>SP: Done
SP-->>App: Return and continue
Reading the sequence
Follow the arrows from top to bottom. The application thread ends a span and, without any pause, finds itself inside the processor, then inside the exporter, then waiting on the network. Only after the backend answers does control travel back up the chain. Every arrow on the way down and on the way back is time during which your application is not doing its real job.
Different language implementations differ in small details. Some call the exporter on the same thread and wait. Some hold a lock so that two threads cannot export at the same moment. The OpenTelemetry specification says that the Simple processor must not let export calls overlap, so the exporter can assume it is never called by two threads at once. The shared idea stays the same: the work happens right there, right then, on the thread that ended the span.
What the Simple processor does not have
It has no queue, no timer, no background worker, and no settings to tune. This is why it is called simple. There is nothing to configure, nothing waiting in memory, and nothing to forget to flush. If you ask it to flush, it has nothing to send. If you ask it to shut down, it only asks the exporter to shut down.
Think of a person who answers each email the second it arrives. Nothing ever piles up in the inbox. But the person never gets a long stretch of focused work, because every email interrupts them. The Simple processor behaves like that person, and the interruptions are the network calls.
The word “Simple” refers to the design, not to the effort of the application. Specification wording describes it as handing spans over to the exporter as soon as they end. Details of locking and waiting are left to each language’s SDK, so check the behavior of the one you use.
7The Simple Span Processor: Strengths and Costs
Every design trades one good thing for another. The Simple processor trades speed for clarity, and it is worth being honest about both sides.
Strengths
- Easy to understand, with no hidden waiting line
- Spans appear at the destination almost immediately, which is great while debugging
- No spans sit in memory waiting, so nothing is lost from a queue if the program stops
- No tuning settings to get wrong
- Behavior is predictable and ordered, which helps tests
Costs
- The application thread pays for the whole export, including the network
- One network call per span is wasteful, so overhead is high
- If the backend is slow or unreachable, your own requests become slow
- Throughput is limited by how quickly one span can be exported
- Not recommended for production traffic by the project’s own guidance
How slow can it get?
Suppose a single export takes ten milliseconds, which is a modest figure for a call across a data center. A request that ends fifteen spans would add roughly one hundred fifty milliseconds of waiting, even though the real work might take forty. In this made-up example, tracing has made the service almost four times slower. If the backend has a bad moment and each export takes a full second, the same request would stall for fifteen seconds. Your users would blame your application, not your telemetry.
This effect is the reason the Simple processor is called a testing and learning tool. The cost is invisible when you have one request per second on a laptop. It becomes painful when you have a thousand requests per second.
Think of a cashier who must personally walk each receipt to the accountant’s office after every sale. With two customers a day, nobody notices. With a line out the door, the shop stops.
The danger is not that the Simple processor is slow when everything is healthy. The danger is that it ties your application’s speed to the health of a system you do not control: the network and the backend.
8The Batch Span Processor: How It Works
The Batch processor adds three new parts to the story: a queue, a timer, and a background worker. Together they take sending out of the application’s way.
When a span ends, the Batch processor checks whether it is sampled. If it is, the processor places the span into an in-memory queue and returns right away. This step takes a tiny fraction of a millisecond. Your application thread goes back to its work.
Meanwhile, a separate background worker, usually a thread or an equivalent in the language, watches the queue. It wakes up in two situations. One is when a timer fires after a set delay. The other is when the queue has grown large enough to fill a whole batch. The worker then removes up to a maximum number of spans from the queue, hands them to the exporter as one list, and waits for the exporter to finish. Then it goes back to watching.
flowchart LR
A1["Request thread 1 ends a span"] --> Q["In-memory queue"]
A2["Request thread 2 ends a span"] --> Q
A3["Request thread 3 ends a span"] --> Q
Q --> W["Background worker"]
T["Timer fires"] --> W
F["Queue holds a full batch"] --> W
W --> E["Exporter sends one batch"]
E --> B["Collector or backend"]
Q -. "queue full" .-> D["New spans dropped"]
Reading the picture
On the left, several request threads each finish spans and drop them into the same queue. Because the drop is a quick in-memory action, none of them waits for the network. In the middle, a single worker is woken by either the timer or a full batch. On the right, the worker gives the batch to the exporter, which sends it as one network message. The dotted line at the bottom is the safety valve: if the queue is already full when a new span arrives, the new span is dropped instead of making the application wait.
Why one worker is enough
Sending a batch of five hundred spans in one message is much cheaper than sending five hundred messages. So a single worker can keep up with an enormous number of spans. Also, one worker means the exporter sees only one export at a time, which keeps the exporter’s job simple.
Picture an office outbox. Everyone drops letters in the tray as they finish, and it takes a second. A mailroom clerk collects the tray whenever it is full or whenever the clock hits the next collection time, and takes the whole load to the post office. Staff never leave their desks, and the post office sees fewer, larger deliveries.
Remember the three triggers for a send: the timer, a full batch, and a flush or shutdown request. Almost every question about Batch behavior comes back to one of these three.
9The Four Knobs of the Batch Span Processor
The Batch processor has settings that the Simple processor does not. Four of them matter most, and they are the same across languages even if the names differ slightly.
| Setting | Plain meaning | Typical default | Effect of raising it |
|---|---|---|---|
| Maximum queue size | How many spans may wait in line | 2048 | Survives longer bursts and slow exports, but uses more memory |
| Scheduled delay | How long the worker waits between timer-driven sends | 5 seconds | Fewer, larger sends, but spans appear later |
| Maximum export batch size | Most spans in one send | 512 | Fewer calls, but larger messages that may hit size limits |
| Export timeout | How long one send may take before it is cancelled | 30 seconds | More patience with a slow backend, but a longer wait before giving up |
Two rules that keep the settings sensible
The first rule is that the maximum batch size should never be larger than the maximum queue size. A batch cannot contain more spans than the line can hold. Many SDKs quietly adjust a bad combination, but do not count on it. The second rule is that the queue should be several times larger than one batch. If the queue is only barely bigger than a batch, a short pause in sending fills it up and spans are dropped.
Where the settings come from
Most SDKs let you set these values in code when you create the processor. The OpenTelemetry project also defines environment variables for them. Their names start with OTEL_BSP_, where BSP stands for batch span processor, followed by the setting name, such as the schedule delay, the export timeout, the maximum queue size, and the maximum export batch size. Using environment variables is popular because you can change behavior without rebuilding the application.
These four settings are like the rules of a school bus. The bus waits at most five minutes at the stop, or leaves earlier when full. It holds a fixed number of seats. A trip longer than a set limit is cancelled. If the stop is crowded and the bus is full, extra students wait for the next day, which is the dropped span.
Default values and names have been stable for a long time, but they can differ by language or version. Treat the numbers in this guide as common defaults and confirm them in the documentation of the SDK you actually use.
10When the Queue Fills: Dropping Spans on Purpose
The Batch processor makes a deliberate choice that surprises many newcomers. When it cannot keep up, it throws spans away rather than slowing your application.
Imagine the worker is busy sending a batch, and the backend is slow. Meanwhile, request threads keep ending spans and adding them to the queue. The queue grows. If it reaches its maximum size, the next span that arrives has nowhere to go. The processor has two options. It could make the request thread wait until there is room, which would bring back the very slowness we wanted to escape. Or it could discard the new span. The OpenTelemetry design chooses to discard, because observability should never be the reason your product becomes unavailable.
What a drop looks like to you
Dropped spans do not raise an error in your application. A trace may arrive with missing pieces, or a whole request may not appear at all. Some SDKs log a warning or expose a counter of dropped spans. Others stay silent. This is why watching the queue, a topic of a later chapter, is so important. Without it you may spend hours wondering why a trace has a hole in it.
Why drops usually happen in bursts
A queue with 2,048 slots sounds large, but a busy service can fill it quickly. Suppose a service ends 4,000 spans per second during a traffic spike, and the exporter can send only 2,000 per second. The queue gains 2,000 spans every second and would be full in roughly one second. After that, half of all spans are dropped until the spike passes. Drops mean that you demanded more output than the exporter and network could deliver.
Normal times
The queue is nearly empty. The worker sends batches faster than spans arrive.
A spike or a slow backend
Spans arrive faster than they leave. The queue starts to grow.
The queue is full
New spans are dropped immediately. Application threads still do not wait.
Recovery
The spike ends or the backend recovers. The queue drains, and drops stop.
A busy mailbox in an apartment lobby has a fixed number of slots. When the mail carrier is late and residents keep dropping in letters, the last letters have nowhere to go. A good building turns away the extra letters rather than making every resident wait in the hallway.
If you see drops, there are three fixes in order of preference: send fewer spans through sampling, make the exporter faster or closer, and only then raise the queue size.
11Timer Sends, Full-Batch Sends, and Flush Sends
The worker can wake for three different reasons. Understanding each one explains why spans arrive when they do.
Reason one: the timer
Every few seconds, by default five, the worker wakes up and sends whatever is in the queue, up to the batch size limit. This guarantees that even a quiet service eventually reports its spans. Without a timer, a service that creates only three spans per minute would never fill a batch and would keep its spans forever.
Reason two: a full batch
When enough spans have piled up to fill a whole batch, the worker does not wait for the timer. It sends right away. This keeps busy services from letting the queue grow when there is work to do. So on a busy service, sends happen much more often than once every five seconds, and the timer matters little.
Reason three: flush or shutdown
When your code calls flush, or when the program shuts down, the worker sends everything it has, in as many batches as needed, as fast as it can. This is the only way to be sure the queue is empty at a chosen moment.
| Trigger | Who starts it | What it protects | Typical situation |
|---|---|---|---|
| Timer | The clock | Freshness of data on quiet services | A low-traffic internal tool |
| Full batch | Span volume | Memory and queue headroom on busy services | A public API at peak hour |
| Flush | Your code or a framework | Spans before a pause or freeze | The end of a serverless invocation |
| Shutdown | Your code or a framework | Spans before the program exits | A deployment that restarts the service |
A small example with made-up numbers
A service ends 20 spans per second. With a batch size of 512, it would take about 25 seconds to fill one batch. So the five-second timer fires first, and each send carries roughly 100 spans. Another service ends 2,000 spans per second. A batch of 512 fills in about a quarter of a second, so sends happen about four times per second and the timer rarely fires. The same settings behave differently depending on traffic, which is a nice feature.
A bus that leaves every thirty minutes, or sooner if full, serves both a quiet village and a busy town with the same rule. In the village it leaves on the clock with a few riders. In the town it leaves as soon as the seats fill.
The scheduled delay is a ceiling on how long a span waits on a quiet service, not a promise. A span on a busy service may be sent within a fraction of a second.
12Head to Head: A Detailed Comparison
Now that both are understood on their own, this chapter lines them up across the questions that matter in real projects.
| Dimension | Simple | Batch |
|---|---|---|
| Moment of export | On span end, immediately | Later, on timer, full batch, flush, or shutdown |
| Thread doing the export | The thread that ended the span | A dedicated background worker |
| Added latency to your code | High, equal to one export per span | Very low, only a queue insertion |
| Network calls | One per span | One per batch |
| Memory use | Almost none | Up to the queue size in spans |
| Behavior with slow backend | Your application slows down | Queue grows, then spans are dropped |
| Behavior on crash | Spans already exported are safe | Spans in the queue are lost |
| Tuning needed | None | Queue, delay, batch size, timeout |
| Visibility of spans at the backend | Near real time | Delayed by up to the scheduled delay |
| Recommended for production | No | Yes |
The central trade
If you read the table carefully, you see that the two processors make opposite trades on two axes. On the axis of application speed, Batch wins by a huge margin. On the axis of never losing a span at the moment of a crash, Simple wins, because it never holds anything back. You cannot have both. Production systems almost always choose speed and stability for the application, since a missing span is a minor loss and a slow checkout is a business problem.
Choose Simple when
- You are learning and want to see spans instantly
- You are writing automated tests of instrumentation
- You are debugging on your own machine with a console exporter
- Volume is tiny and delay does not matter
Choose Batch when
- The service handles real user traffic
- Latency and throughput matter
- The backend is across a network and may sometimes be slow
- You want efficient, larger network messages
Choosing between the two is like choosing between hand delivery and a courier service. Hand delivery gets one parcel to its destination right now, and it ties up your day. A courier takes a day longer but handles a thousand parcels while you stay at work.
13Threads and Waiting: What Your Request Actually Feels
The biggest practical difference is invisible in a list of settings. It is about which thread pays the price of sending.
A thread is a single line of work inside a program. A web server might use hundreds of threads at once, each serving one user’s request. When a thread is waiting for the network, it cannot do anything else. In many servers the number of threads is limited. If many threads wait on slow exports, there are fewer free threads for new requests, and users see delays or errors.
With the Simple processor, the request thread is the one that waits. Tracing therefore uses up your capacity to serve users. With the Batch processor, the request thread only adds a span to the queue. The background worker, which is separate from your capacity to serve users, pays for the network. Your server can handle just as many requests with tracing on as with tracing off, apart from a tiny cost.
The effect on tail latency
People who run busy systems care about tail latency, the slowest few percent of requests. Under the Simple processor, a single slow export makes one request slow. If the backend has a hiccup, many requests become slow at once. The tail of your latency chart then reflects the telemetry system’s health instead of your application’s health. This is a surprising and unpleasant coupling, and it is the main reason experienced engineers avoid the Simple processor outside of testing.
flowchart TB
subgraph SIMPLE["Simple processor"]
S1["Request thread does work"] --> S2["Request thread exports span and waits"]
S2 --> S3["Request thread responds to user"]
end
subgraph BATCH["Batch processor"]
B1["Request thread does work"] --> B2["Request thread enqueues span"]
B2 --> B3["Request thread responds to user"]
B2 -.-> B4["Worker exports later on its own thread"]
end
Reading the picture
In the top box, the export step stands between doing the work and answering the user, so the user’s wait includes it. In the bottom box, the request thread finishes right after the quick enqueue step. The worker’s export, shown with a dotted arrow, happens off to the side and the user never feels it.
A bank teller who must walk each signed form to the vault before serving the next customer makes the line crawl. A bank where tellers drop forms in a tray and a runner takes them to the vault keeps the line short. The customers in line are your users.
If a service seems to get slower the moment tracing is turned on, check the processor first. A Simple processor in production is a frequent culprit.
14A Worked Example With Made-Up Numbers
Numbers make ideas stick. The following example is invented for teaching, and the exact figures will differ in your system.
The made-up service: “Pebble Orders”
Pebble Orders is an imaginary order service. It handles 300 requests per second at peak. Each request creates 12 spans. That is 3,600 spans per second. The service runs 200 threads. The backend is in the same region, and a single export call takes about 8 milliseconds when the backend is healthy.
Case A: Simple processor
Each request must pay for 12 exports at 8 milliseconds each, which is 96 milliseconds of extra waiting. If real work takes 30 milliseconds, a request now takes 126 milliseconds, more than four times longer. The service needs many more threads to serve the same traffic, because each thread is held much longer. If the backend slows to 40 milliseconds per export, each request adds 480 milliseconds, and the service may run out of threads and begin rejecting users. The 3,600 network calls per second are also heavy for the backend.
Case B: Batch processor with defaults
The service produces 3,600 spans per second, so a batch of 512 fills in about 0.14 seconds. The worker sends about seven batches per second. Even if each batch takes 20 milliseconds to send, the worker is busy for only 140 milliseconds per second, which is comfortable. Request threads add 12 spans each, costing a tiny amount of time. Latency is nearly the same as with tracing off.
Case C: a backend hiccup for 10 seconds
The backend stops answering for 10 seconds. Spans keep arriving at 3,600 per second, but the queue holds only 2,048, which is about 0.57 seconds of traffic. So within the first second, the queue fills, and most spans for the next 9 or so seconds are dropped. Users notice nothing. The team later sees a gap in the traces. They could raise the queue size, but 10 seconds of spans would need around 36,000 slots, which costs memory. A better answer might be to sample fewer traces, or to place a Collector close to the service that absorbs short backend outages.
| Measure (made-up) | Simple | Batch |
|---|---|---|
| Extra time per request | About 96 ms | Well under 1 ms |
| Network calls per second | 3,600 | About 7 |
| Effect of 10 second backend outage | Requests stall or time out | Some spans dropped, users unaffected |
| Memory for tracing | Negligible | Up to about 2,048 spans |
Pebble Orders is not a real company. The point of the example is the shape of the numbers. A few thousand tiny calls become a handful of larger ones, and the user’s wait shrinks from tens of milliseconds to almost nothing.
15Memory and CPU: The Hidden Costs
Nothing is free. The Batch processor spends a little memory to save a lot of time, and it is worth knowing what that exchange looks like.
Memory
A queue of 2,048 spans is not just 2,048 small numbers. Each span holds its name, its identifiers, timestamps, attributes, events, and links. A span with a few attributes might take a kilobyte or two. A span with large attributes, such as a long query text, can take much more. So a full queue may occupy several megabytes, or tens of megabytes in unusual cases. For most services this is small, but for tiny containers with strict limits, it deserves a thought. The Simple processor holds nothing, so it has no such cost.
CPU
The exporter must turn spans into a message, which takes processor time. Batching gives it an advantage here. Packing 512 spans into one message shares overhead such as headers and connection setup. Compressing a bigger message is also usually more efficient than compressing many tiny ones. So, counter-intuitively, the Batch processor often uses less total processor time than the Simple one, even though it has extra moving parts.
Network
One network message per span means many small packets and many connection round trips. One message per batch means fewer, larger packets, which use the network and the receiving system more efficiently. Receivers such as Collectors are designed with batches in mind and run best when they receive them.
Batch uses more
Holds up to a full queue of spans at once.
Batch often uses less
Shares message overhead and compresses better across many spans.
Batch uses fewer calls
One call can carry hundreds of spans.
Batch is far gentler
The request thread never waits for the network.
Shipping one box of fifty books costs far less than fifty separate parcels. You need a little floor space to stack the books before they go, which is the memory. The savings in packing, stamps, and trips are much bigger.
In memory-tight containers, reduce the queue size moderately and reduce span attribute sizes, instead of switching to the Simple processor. Moving to Simple would trade a small memory problem for a big latency problem.
16Failure Scenarios: What Breaks and How Each One Reacts
Systems are judged by how they behave on bad days. Here are the common failures and how each processor responds.
| Failure | Simple processor | Batch processor |
|---|---|---|
| Backend slow | Every ended span stalls the application thread | Queue grows, worker is slower, drops possible if it fills |
| Backend down | Each export waits until it fails or times out, then the span is lost | Each batch fails after the timeout and is lost, new spans queue or drop |
| Network cut | Same as backend down, but on the request path | Same as backend down, off the request path |
| Traffic spike | Many exports in parallel threads, though serialized in some SDKs, causing long waits | Queue absorbs the spike up to its size |
| Exporter bug that throws errors | Error occurs on the request path and should be caught by the SDK | Error occurs in the worker and does not touch requests |
| Oversized message rejected | Unlikely, since messages hold one span | Possible if batches are too large for the receiver, so lower the batch size |
Retries and what happens to failed batches
Many exporters retry a failed send a few times with growing pauses, especially for errors that look temporary. The retry behavior belongs to the exporter, not to the processor. Retries help with short hiccups. They also keep the worker busy longer, so the queue grows while it waits. In the end, if the send still fails, the batch is dropped. The span processors themselves do not store spans on disk, so there is no durable safety net.
A reasonable safety net
To protect against backend trouble without paying a latency cost, teams often send spans to a nearby Collector first. The Collector is a separate program with its own queues, its own retry behavior, and sometimes disk-backed buffers. The application’s Batch processor only has to hand batches to the local Collector, which is fast and nearly always available.
If the highway to the city is closed, a single truck driver waits in the traffic jam, and so does everything the truck is carrying. A nearby depot, which is the Collector, takes your deliveries instantly and handles the long trip, patiently, on its own schedule.
Neither processor guarantees delivery. If you need guaranteed delivery of every record, tracing is the wrong tool, and you should use a durable system such as a message queue for those records.
17Crashes and Shutdown: Where Queued Spans Go to Vanish
The Batch processor’s one real weakness is that spans waiting in the queue exist only in memory. When the program ends, so do they, unless you flush.
Two ways a program ends
A graceful exit is when your program is told to stop and has time to tidy up, such as when a deployment system sends a polite stop signal. A hard crash is when the program dies suddenly, because of a memory failure, a forced kill, or a power loss. For a graceful exit, you can call shutdown on the tracer provider, and the processor will flush its queue first. For a hard crash, nothing runs, and whatever sat in the queue is gone.
flowchart TB
START["Program receives stop signal"] --> HOOK{"Is a shutdown hook registered?"}
HOOK -- "yes" --> FL["Provider shutdown flushes the batch queue"]
FL --> EXPORT["Exporter sends remaining spans"]
EXPORT --> EXIT["Program exits cleanly"]
HOOK -- "no" --> LOST["Queued spans are lost"]
LOST --> EXIT2["Program exits"]
CRASH["Hard crash or forced kill"] --> LOST
Why the lost spans often matter most
The spans that were in flight right before a crash are exactly the ones you want to read when investigating the crash. That irony is real. Teams reduce the risk in two ways. They call shutdown on every graceful exit, and they keep the scheduled delay short enough that the exposure window is small. A shorter delay means fewer spans are at risk, at the price of more frequent, smaller sends.
What the Simple processor offers here
The Simple processor has already sent everything that ended before the crash. Only the span being exported at that exact instant is at risk. That is a real advantage for crash debugging, but it does not justify the latency cost on production services. A better answer for crash visibility is to log crash details separately, and to keep the Batch delay modest.
A shop that keeps the day’s takings in the till, and banks them at closing time, is fine until a robbery happens at noon. A shop that banks every hour loses less. A shop that walks every single sale to the bank loses nothing, but sells very little.
Check that your framework or runtime calls provider shutdown on exit. Many frameworks offer a hook for this. If yours does not, register the call yourself, and then test it by stopping the service and checking that the last spans arrive.
18Short-Lived Programs and Serverless: The Flush Problem
Some programs live for a few seconds, or even a few milliseconds. For them, the timing of the Batch processor can quietly swallow every span.
Command-line tools and batch jobs
Imagine a small program that starts, does a task in two seconds, and exits. With the Batch processor and a five-second delay, the timer never fires before the program ends. If the program does not flush or shut down properly, the queued spans vanish, and the trace viewer shows nothing. The fix is to call shutdown or flush on the tracer provider right before exiting. This one habit prevents most “my spans never show up” tickets for scripts.
Serverless functions
Serverless platforms run your code only when a request arrives, and they may freeze the process the moment your function returns. A frozen process cannot run its background worker. So spans left in the Batch queue wait until the next time the function is thawed, which may be minutes later, or they may be lost if the environment is discarded. To avoid this, serverless setups usually flush at the end of every invocation, before returning the response. Some teams use the Simple processor here, accepting the small cost because each invocation creates few spans. Others flush a Batch processor explicitly and keep the cost low. The right choice depends on span count per invocation and the platform’s behavior, so check the platform’s documentation.
| Program type | Main risk with Batch | Common remedy |
|---|---|---|
| Long-running web service | Drops during spikes or outages | Sampling, a local Collector, queue sizing |
| Command-line tool | Exits before the timer fires | Call shutdown before exit |
| Scheduled batch job | Same as above | Shutdown at the end of the job |
| Serverless function | Process frozen before export | Flush at the end of each invocation |
| Short test script | Spans missing in output | Use Simple, or flush explicitly |
A traveler who keeps postcards in a tray for the hotel to mail later should drop them at the front desk before checking out. If they leave without doing so, the postcards stay on the desk forever. Flushing is the act of handing them over on the way out the door.
A flush can take as long as the exporter needs, up to its timeout. In serverless, where every millisecond is billed, a slow flush has a direct cost. Place the Collector or backend close to the function to keep flushes short.
19Testing and Debugging: Where Simple Shines
The Simple processor earns its place in the toolbox. In a test or in a first experiment, immediate and ordered output is exactly what you want.
Learning and first experiments
When you are trying OpenTelemetry for the first time, you want to run your program and see spans appear in the terminal at once. Pair the Simple processor with a console exporter, which prints spans as text, and you get that instantly. There is no waiting five seconds and no wondering whether a timer will fire.
Automated tests
A test of instrumentation wants to run some code and then check which spans were created. Most SDKs provide an in-memory exporter that keeps spans in a list. Paired with the Simple processor, the list is complete the moment the code under test finishes. With the Batch processor, the test must remember to flush first, or the list may be empty or incomplete and the test becomes unreliable. Using Simple keeps tests short and stable.
Local debugging of a single request
When you chase one strange request on your own machine, you may want spans to show up in the backend in real time. The Simple processor does that. Just do not leave it enabled when you commit the change to a shared environment.
Experiment locally
Simple processor with console exporter. You see every span instantly.
Write tests
Simple processor with an in-memory exporter. Assertions are reliable and fast.
Try it in a shared environment
Switch to the Batch processor with an OTLP exporter and a Collector.
Run in production
Batch processor, tuned and monitored, with shutdown wired in.
A cook tastes the soup with a small spoon after every pinch of salt while experimenting. When cooking for two hundred guests, that approach is impossible, and the cook trusts a tested recipe. The Simple processor is the tasting spoon, and the Batch processor is the tested recipe.
Make the processor choice depend on configuration, such as an environment name, so that the same code uses Simple in tests and Batch in real environments without manual edits.
20How to Choose: A Simple Decision Path
Instead of memorizing rules, follow a path of questions. Most real cases are settled in two or three steps.
flowchart TB
START["Which span processor should I use?"] --> Q1{"Is this a test or a local experiment?"}
Q1 -- "yes" --> SIMPLE["Use the Simple processor"]
Q1 -- "no" --> Q2{"Does the program run longer than a few seconds?"}
Q2 -- "yes" --> BATCH["Use the Batch processor and wire in shutdown"]
Q2 -- "no" --> Q3{"Serverless or short job?"}
Q3 -- "yes" --> Q4{"Few spans per run?"}
Q4 -- "yes" --> EITHER["Simple, or Batch with an explicit flush"]
Q4 -- "no" --> BATCH2["Batch with an explicit flush before exit"]
Q3 -- "no" --> BATCH
Walking the path
The first question catches the easy case. If it is a test or a local experiment, Simple is the friendly choice. If not, ask how long the program lives. A web service that runs for days should use Batch and make sure shutdown is wired in. A short program raises a second question. In a serverless function or a short job that makes only a few spans, the Simple processor is acceptable, because the extra waiting is small and nothing is left behind. If the program makes many spans per run, use Batch and flush before the exit.
Beyond the path: two extra checks
First, ask how bad it would be to lose a few spans. For most services, a few missing spans are fine. For auditing needs, tracing is not the right tool anyway. Second, ask where the backend is. A backend across the world pushes the Batch processor toward larger batches and a local Collector, and pushes the Simple processor into pain.
Choosing a moving method depends on the load. One bag you carry yourself. A whole house needs a truck, and the truck must be told when to leave and what to take. The decision path is your checklist before calling a mover.
Many official getting-started examples use a Simple processor with a console exporter, since it prints right away. That is a teaching convenience, not advice for production.
21Using More Than One Processor
A tracer provider can hold a list of processors. Each processor sees every span event, and this opens useful combinations.
Processors that change spans on start
Besides the two exporting processors, you can write a custom processor that acts on the start moment. For example, it might add the current deployment version or the name of a feature flag to every span as it begins. Because it works at start, the span is still open and can be changed. Such a processor does not export anything, so it is paired with a Simple or Batch processor that does.
Processors that filter or redact
A custom processor can also decide to hide or alter data before it reaches the exporter. For example, it might remove an attribute that should not be shared. Whether this is possible on the end moment depends on the SDK, since ended spans are read-only. Some SDKs offer a wrapping approach, where one processor passes a modified copy of the span to the next. If you need dependable removal of sensitive fields, a common and more reliable place is the Collector, which has processors designed for the purpose.
Fan-out to several destinations
You can register one Batch processor for your production backend and a second Batch or Simple processor for another destination, such as a local file. Each has its own queue, so a slow destination affects only its own queue. This is a clean way to isolate trouble.
flowchart LR
SPAN["Span events"] --> ENR["Enriching processor on start"]
SPAN --> BP["Batch processor"]
SPAN --> SP["Simple processor"]
BP --> EX1["OTLP exporter to Collector"]
SP --> EX2["Console exporter for debugging"]
In this picture, all three processors receive each span event. The enriching processor changes spans as they start. The Batch processor sends to the Collector. The Simple processor prints to the console, which might be turned on only for a debugging session.
At a race finish line, one official records the time in the official book, one announces results to the crowd, and one takes photos. They all watch the same runners, and each does a different job at their own pace.
Each additional exporting processor adds its own cost: another queue and another round of exports. Do not add destinations casually, and remember that the order of processors may matter for custom ones that change data.
22Two Things Called “Batch”: SDK Processor Versus Collector Processor
The word batch appears in two different places in the OpenTelemetry world, and they are easy to confuse.
The Batch Span Processor lives inside your application’s SDK. It is the one this guide is about. It groups spans before they leave your program.
The OpenTelemetry Collector is a separate program that receives telemetry, changes it, and forwards it. The Collector also has a component named the batch processor, which groups telemetry before it is sent onward to the next destination. Although the names are close, they work at different places on the path, with different settings, and they solve related but separate problems.
| Question | SDK Batch Span Processor | Collector batch processor |
|---|---|---|
| Where does it run? | Inside your application | Inside the Collector program |
| What does it protect? | Your application’s speed | The efficiency of the Collector’s outgoing sends |
| Which signals? | Spans only | Traces, metrics, and logs |
| Settings style | Code or OTEL_BSP environment variables | Collector configuration file |
| Can it lose data? | Yes, drops when its queue is full | Depends on the Collector’s other settings, since it may limit memory use |
Do I need both?
Commonly, yes. The SDK processor protects your application from the network. The Collector’s own batching protects the Collector’s exporters and the final backend from a flood of small messages that came from many applications. They are two links in the chain, each doing a separate job. The Collector’s components and defaults change from release to release, so check its current documentation before copying a configuration.
A neighborhood has a small mailbox at each house where letters are collected, and a regional sorting center that bundles bags for long-distance trucks. Both bundle mail. They are separate steps, and removing one does not remove the need for the other.
When someone says “turn on batching,” ask which one they mean. Fixing the wrong one wastes an afternoon.
23Sampling and the Processor: Who Sees Which Span
Sampling and processing are neighbors in the pipeline. Understanding their order stops a lot of confusion about missing spans.
Sampling is the decision of which traces to record. A sampler makes this decision when a span starts. If the sampler says “do not record,” the span is either not created at all, or created as a lightweight placeholder that is never exported. Only spans marked as sampled continue on to be exported.
The processors receive start and end events, but the export processors, Simple and Batch, export only sampled spans. This means sampling reduces the load on both processors. With a sampling rate of ten percent, the Batch queue sees ten percent of the spans, so it fills much more slowly. With the Simple processor, ten percent sampling means ten percent as many expensive exports, which is a big improvement, though still not enough to make Simple a production choice.
Span starts
The sampler decides whether this trace is recorded.
Span runs
If recorded, it collects attributes and events. If not, it costs almost nothing.
Span ends
The processor receives the end event.
Processor handles it
Simple exports now, and Batch enqueues. Only sampled spans take part.
A common misunderstanding
Some people think that the Batch processor decides which spans to keep when the queue is crowded. It does not. It drops the newest arrivals when full, without looking at their importance. If you want to keep all errors and slow traces and drop the boring ones, you need a smarter sampling strategy, such as tail sampling in a Collector, where the whole trace is visible before the decision.
A museum curator decides which items go on display, which is sampling. The delivery crew decides when and how to bring the chosen items into the gallery, which is processing. A crowded delivery dock does not turn a masterpiece into a spare part. It simply turns away whatever arrives last.
If spans are missing, check sampling first and processor drops second. The cure for each is quite different.
24Ordering and Concurrency Guarantees
When many threads end spans at once, what order do spans leave in? The answer is gentler than many people fear, and it matters less than they think.
Order inside the Simple processor
Because the Simple processor exports at the moment of ending, the order of exports follows the order in which spans end, as far as threads can be ordered at all. When two threads end spans at nearly the same instant, one of them waits for the other, so exports never overlap. This keeps things orderly, though it can serialize your application threads, which is another reason for lower throughput.
Order inside the Batch processor
The queue preserves the order in which spans were added, so batches usually follow end order. But different applications, different threads, and retries mean the backend can still receive data in a slightly different order than it was produced. Backends are built to handle this. They sort spans by their timestamps and by their parent and child links, not by arrival order.
Why parent spans often arrive after children
A parent span ends after all its children, because it covers their whole duration. Since processors work on ended spans, the children are exported before the parent. A viewer must therefore wait for the parent, or draw the trace with a missing root for a moment. This is normal behavior, and backends cope with it by waiting briefly before showing a trace as complete.
sequenceDiagram
participant C1 as Child span A
participant C2 as Child span B
participant P as Parent span
participant Q as Processor
C1->>Q: Ends first
C2->>Q: Ends second
P->>Q: Ends last
Note over Q: Children reach the exporter before the parent
The picture shows two child spans ending first, then the parent. Whatever the processor, the exporter receives them in that order. A trace viewer reconstructs the tree afterward by following parent links, so the arrival order does not harm the final picture.
When a book is assembled, chapters may arrive at the printer in any order. The page numbers and the table of contents tell the printer how to put them back in sequence. Spans carry their own identifiers in the same way.
Never write code or tests that depend on spans arriving at the backend in a precise order. Depend on identifiers and timestamps instead.
25Monitoring the Processor Itself
The thing that reports on your system needs reporting on too. A silent processor is one of the hardest problems to find.
What to watch
Queue size
How many spans are waiting right now. A queue that stays near its maximum means exports cannot keep up.
Dropped spans
How many spans were thrown away because the queue was full. Any steady number above zero deserves attention.
Export duration
How long each send takes. A rising trend warns of backend or network trouble.
Export failures
How many sends failed or timed out. Failures turn into lost batches.
Where these numbers come from
Some SDK versions can expose the processor’s own metrics, such as queue size and processed spans, through OpenTelemetry’s metrics, and many log warnings when spans are dropped. Support differs across languages and versions, so check what yours offers. If the SDK does not provide them, a fallback is to watch for gaps from the other side. Compare the number of requests your service handled, taken from your web server’s own metrics, with the number of root spans that arrived at the backend. A persistent gap suggests drops or sampling.
If you use a Collector
The Collector exposes its own metrics about data received, refused, and sent. Watching them tells you whether your batches are arriving, and whether the next link in the chain is also struggling. Seeing trouble in both places points to the backend. Trouble only at the application side points to the network between the application and the Collector, or to sizing.
A mailroom should keep a simple log of how many letters came in, how many went out, and how many were returned. If the log shows that incoming mail keeps exceeding outgoing mail, you know that a problem is building before anyone complains about late letters.
Alert on a rising drop count, not only on a full queue. A queue can be full for one second during a spike without trouble. Drops that continue for minutes mean a real mismatch between output and capacity.
26Security and Privacy Considerations
Processors handle every span your application creates, so they sit at a sensitive crossroads. A few careful habits keep that crossroads safe.
What is inside spans
Spans can carry attributes such as user identifiers, query text, URLs, and header values. If sensitive information such as a password, a card number, or a health detail ends up in a span, then the processor will faithfully deliver it to every destination. The processor does not know what is secret. The rule is to keep sensitive values out of spans in the first place, and to add a safety net where possible, such as removing known fields in a custom processor or in the Collector.
Spans waiting in memory
The Batch queue holds spans in memory for a short time. This is not a major security risk by itself, since the same memory already holds your data. But if a memory dump of the process is taken for debugging, queued spans are in it. Treat such dumps as sensitive.
Protecting the path out
- Encrypt in transit. Use TLS between the exporter and the Collector or backend, especially across networks you do not control.
- Authenticate the sender. Receivers should accept spans only from known senders, using tokens or certificates.
- Limit sizes. A sensible maximum batch size and attribute size limits protect against oversized messages and accidental data leaks of huge values.
- Prefer a local Collector. Applications talk to a Collector in the same trusted network, and only the Collector holds credentials for the outside world.
A denial of service angle
Because a Simple processor ties request speed to the backend, an attacker who can slow your backend, or flood your tracing with traffic, may indirectly slow your application. The Batch processor’s drop-instead-of-wait policy reduces this risk, since a flooded queue never blocks users.
A mailroom clerk delivers whatever is in the envelope without opening it. If someone put a secret in the envelope, the clerk cannot know. Good security starts with not putting secrets in the mail, and then using a locked bag for the trip.
Review a sample of real spans from production once in a while, just as you would review log lines. Surprises are common, and finding one early is much cheaper than finding it after an audit.
27Good Patterns
Experienced teams repeat the same few habits. Each pattern below solves a real, common problem.
Batch with a local Collector
The application sends batches to a Collector on the same machine or in the same network. The Collector handles retries and the long trip. Application exports stay fast and rarely fail.
Environment-driven choice
The processor type is decided by configuration. Tests use Simple with an in-memory exporter. Real environments use Batch.
Shutdown wired to exit
A hook calls provider shutdown when the process is told to stop, so the queue is flushed before exit.
Flush at the end of work
Short jobs and serverless functions flush at the end of each run instead of relying on the timer.
Sample first, then batch
Reduce the volume with sampling so that the queue stays calm and exports stay small and cheap.
Watch the drops
Dashboards and alerts show queue size, dropped spans, and export duration, so problems are seen early.
Why the first pattern deserves extra attention
Putting a Collector next to the application changes the failure story. If the far-away backend goes down for an hour, the Collector can buffer and retry, and the application never notices. The application’s Batch processor talks to something nearby that answers within a millisecond or two. This moves the difficult part of reliability out of every application and into one shared, well-tuned program.
A neighborhood drop box for packages means you never drive across the city to the main depot. The drop box owner worries about weather, truck delays, and long routes, and you only walk to the corner.
None of these patterns requires exotic tools. They are small decisions that, taken together, make tracing boring and dependable, which is the best compliment for infrastructure.
28Anti-patterns
The following habits look harmless and cause trouble. Learning them early saves many late-night investigations.
Simple processor in production. It looks fine in a quiet staging environment and hurts under load. Every request pays for its own exports, and a slow backend becomes a slow application.
No shutdown or flush. Batch spans wait in memory and vanish when the program exits. Short jobs show no traces at all, which leads people to blame the backend.
Huge queues to hide a slow exporter. A very large queue turns a throughput problem into a memory problem and a freshness problem. Spans arrive minutes late, and a crash loses far more.
Tiny delays to see spans faster. Setting the scheduled delay to a few milliseconds makes the Batch processor behave like a worse Simple processor, with many small sends and wasted effort.
Ignoring drops. Nobody watches the counters, so gaps in traces are discovered during an incident, when the data is needed most.
Every span to every destination. Many exporting processors, each with its own queue, multiply memory and network use without anyone choosing it deliberately.
Each anti-pattern is like a junk drawer habit. Tossing things in is easy today and costs you an hour of searching next month. A little discipline now keeps the drawer useful.
29Best Practices and Common Mistakes
This chapter is the checklist to keep near your keyboard.
Best practices
- Use the Batch processor for every service that handles real traffic.
- Use the Simple processor only for tests, demos, and local debugging.
- Call shutdown on graceful exit, and flush at the end of short-lived work.
- Keep the default settings until you have a measured reason to change them.
- Keep the maximum batch size well below the queue size, and below what the receiver accepts.
- Send to a nearby Collector rather than straight across the internet.
- Use sampling to control volume before you resize queues.
- Monitor queue size, drops, and export duration.
- Keep sensitive data out of span attributes.
Common mistakes and their cures
| Mistake | Symptom | Cure |
|---|---|---|
| Forgot shutdown in a script | No spans for short runs | Shut down the provider before exit |
| Simple processor left on after testing | Service slows when tracing is on | Use Batch outside tests |
| Batch size larger than receiver limit | Exports rejected with a size error | Lower the batch size |
| Export timeout longer than shutdown grace period | Spans lost during deployment | Shorten the timeout or lengthen the grace period |
| Queue too small for bursts | Drops during spikes | Sample, add a Collector, then consider a larger queue |
| Expecting spans instantly | Spans look late by seconds | Remember the scheduled delay, which is normal |
| Confusing SDK batching with Collector batching | Changes made in the wrong place | Check where each setting lives |
Pilots use checklists not because they forget how to fly, but because even experts miss small steps under pressure. This list does the same for tracing setup.
Whenever you change a processor setting, change only one thing at a time, and look at drops and export times before and after. Guessing at several settings at once makes results impossible to read.
30Real-World Style Examples
The stories below are invented for teaching. They describe situations that teams meet often, and no real company is meant.
Example one: the demo that was too slow
A team of four builds a prototype shop. They copy a getting-started example that uses the Simple processor. In testing, everything is fast. At a customer demo with a few dozen simulated users, pages take four seconds. Profiling shows that most of the time is spent in span exports. They switch to the Batch processor, and pages return to a quarter of a second. The lesson is that examples are made to teach, not to run under load.
Example two: the nightly job with no traces
A company runs a nightly report generator that finishes in ninety seconds. Engineers add tracing with the Batch processor and see traces from their laptops but not from the real job. The job exits right after finishing its work, with spans still waiting in the queue. Adding a provider shutdown call at the end makes every trace appear. The lesson is that short programs must flush.
Example three: the gap during a backend upgrade
A platform team upgrades its tracing backend, which is unavailable for twelve minutes. Services that send straight to the backend show drops and gaps in their traces for that period. Services that send through a local Collector, which has a larger buffer and retries, lose almost nothing. The team makes the Collector the standard path for every service. The lesson is that a nearby buffer is cheap insurance.
Example four: the serverless freeze
A function receives events, creates a few spans, and returns. The Batch processor is used with default settings. Traces appear erratically and in bunches, sometimes minutes after the event. The platform freezes the process after each return, so the worker never gets to send on time. The team adds a flush before the return and moves the Collector close to the function. Traces now appear within a second. The lesson is that background workers do not run in frozen processes.
These stories remind us of learning to drive. Almost every new driver makes the same few mistakes with the clutch, the mirrors, and the parking. Each mistake teaches a rule, and the rules are the same for everyone.
All four examples are fictional and simplified. Real situations usually mix several causes, so always confirm your own diagnosis with measurements.
31Troubleshooting: Where Did My Spans Go?
When spans are missing, work from the inside out. The checklist below follows the path a span takes, so the first break you find is the cause.
flowchart TB
START["Spans are missing at the backend"] --> A{"Was the trace sampled?"}
A -- "no" --> A1["Adjust the sampler or the sampling rate"]
A -- "yes" --> B{"Does the program exit quickly?"}
B -- "yes" --> B1["Add shutdown or flush before exit"]
B -- "no" --> C{"Are drops reported or the queue full?"}
C -- "yes" --> C1["Reduce volume or speed up exports then resize"]
C -- "no" --> D{"Do export errors appear in logs?"}
D -- "yes" --> D1["Fix endpoint or credentials or TLS or size limits"]
D -- "no" --> E{"Is the Collector receiving data?"}
E -- "no" --> E1["Check network path and ports"]
E -- "yes" --> E2["Check Collector pipelines and the backend"]
Explaining the checklist
Start with sampling, because it is the cheapest to check and the most common cause. If traces were never sampled, nothing downstream matters. Next, ask whether the program ends before the timer can fire, which is the classic short-job problem. Third, look for drops or a full queue, which means output exceeds capacity. Fourth, search the application logs for export errors, such as a wrong address, a rejected credential, a failed secure connection, or a message that is too large. If none of these shows a problem, check the Collector and the backend, in that order.
A useful trick is to temporarily add a second processor, a Simple one with a console exporter, to see whether spans are being created and ended at all. If they print on the screen, the problem is downstream of the processor. If they do not, the problem is in your instrumentation or sampling. Remove the extra processor when you are done.
If a parcel never arrives, you check whether it was ever shipped, whether the shipping office closed early, whether the truck broke down, and whether the receiving desk has it. You trace the route in order, and the first missing step is your answer.
Turn on the SDK’s own diagnostic logging while troubleshooting. Most SDKs have a switch for it, and it often states the exact reason for a failed export.
32The Life of One Span Inside the Batch Processor
To make every earlier idea concrete, here is one span followed from the moment it ends until the backend acknowledges it.
The span ends
Your code finishes a step, such as a database query. The span now has its final duration and is read-only.
The sampling check
The processor sees that the span belongs to a sampled trace. If it did not, the story would end here.
Entering the queue
The processor tries to place the span in the queue. If there is room, it goes in within a tiny fraction of a millisecond. If the queue is full, the span is dropped and the story ends here too.
Waiting
The span waits behind older spans. How long depends on traffic. On a busy service it may be a fraction of a second. On a quiet service it may be up to the scheduled delay.
The worker wakes up
The timer fires, or a full batch is waiting, or a flush was requested. The worker takes up to the maximum batch size of spans from the front of the queue.
The exporter packs and sends
The exporter turns the group into one message, perhaps compresses it, and sends it over the network, waiting no longer than the export timeout.
Acknowledgment or failure
If the receiver accepts the message, the spans are safe. If the send fails after any retries, the batch is discarded and the worker moves on.
The span does not travel alone
The span shares this journey with hundreds of neighbors. Its fate is tied to theirs: if the batch succeeds, all succeed, and if it fails, all fail together. This is one more reason to keep batches modest. A giant batch that fails loses a giant amount of data at once, while a smaller batch loses less per failure and is easier for receivers to accept.
Where each cost appears
Look again at the seven stages. Stages one to three happen on your application thread and take almost no time. Stages four to seven happen outside your application’s request path. The only real waiting a request ever sees is the brief handoff in stage three. This is the entire design benefit in one sentence: move the slow stages away from the people who are waiting.
Think of a passenger on a bus. The passenger steps onto the bus, which is quick, then waits for the bus to fill or for departure time, then rides together with other passengers, and either arrives safely with the group or is delayed with the group. Individual passengers do not control the bus, but the system makes each ride cheap.
Some implementations keep the exporting step on a worker thread and wait for it before starting the next batch, so at most one send is in flight at a time. Others may allow more. This guide describes the common single-worker design, and your SDK’s documentation gives the exact details.
33Settings Across Languages and Frameworks
The two processors exist in every mainstream OpenTelemetry SDK, but each language has its own style and its own small surprises.
The same ideas, different packaging
Whether you use Java, Python, JavaScript, Go, .NET, or another language, you will find a simple processor and a batch processor with the names you now know. The batch version takes the same four settings, though names and units differ a little. Some use milliseconds, others seconds. Some call the delay an interval. Reading the documentation of your own language for the precise names takes a minute and avoids a lot of confusion.
Automatic setup and agents
Many teams do not create processors by hand. They use an auto-instrumentation agent or an auto-configuration feature, which reads environment variables and builds a standard pipeline. In such setups, the default span processor is almost always the Batch processor, paired with an OTLP exporter. The environment variables that begin with OTEL_BSP_ let you tune the four settings without changing code. Variables that begin with OTEL_TRACES_EXPORTER pick the exporter. Check the documentation for your agent, because some agents offer an option to select the simple processor for debugging.
Frameworks that manage lifecycle
Some application frameworks create the tracer provider for you and call shutdown when the application stops. This is helpful and removes the need to wire your own hook. Others leave it to you. When you move to a new framework, one of the first checks should be whether shutdown is called, and the easiest way to test it is to stop the service and see whether the very last spans arrive.
| Setup style | Who picks the processor | What you should confirm |
|---|---|---|
| Hand-written setup in code | You | That you chose Batch and registered shutdown |
| Auto-instrumentation agent | The agent, usually Batch | The environment variables used for tuning |
| Framework integration | The framework | That shutdown is called when the service stops |
| Serverless layer or wrapper | The wrapper | Whether it flushes at the end of each invocation |
Different countries put the same road rules in different words, and some junctions have local customs. A careful driver learns the rules of the road once and then checks the local signs when arriving somewhere new.
Do not assume that an example from one language will behave the same in another, especially around shutdown and flush. Always run a small test in your own environment.
34Frequently Asked Questions
The Simple processor exports each span immediately on the thread that ended it. The Batch processor queues spans and a background worker exports them in groups. Simple is easy but slows your code. Batch is efficient but can drop spans when overloaded and loses queued spans on a hard crash.
The Batch processor, in almost every case. Use Simple for tests, demos, and local debugging.
Yes, in three ways. The queue can fill and drop new spans. A batch can fail to send after retries. And spans in the queue are lost if the program dies without a flush.
Commonly a queue of 2048 spans, a delay of 5 seconds between timer sends, a maximum batch of 512 spans, and an export timeout of 30 seconds. Check your language’s SDK documentation, because details can differ.
That is the Batch processor waiting for its timer or for a full batch. It is normal. Lower the scheduled delay slightly if you must, but avoid extremes.
Most likely it exited before the Batch processor sent its queue. Call shutdown or flush on the tracer provider before the program ends.
It holds nothing back, so it loses far fewer spans at a crash. However, if an export fails, that span is lost too, and a slow exporter slows your application.
Not required, but a nearby Collector is a strong pattern. It adds buffering, retries, and a central place to filter or redact data.
No. It lives in the Collector and batches data before the Collector sends it onward. The SDK’s Batch Span Processor lives in your application. Many setups use both.
Only after other options. First reduce volume by sampling and make exports faster. A larger queue costs memory and delays data, and it only helps with short bursts.
Yes, the idea is very similar. There are simple and batch versions for log records, with the same trade-offs.
Yes. Each receives every span event. A common use is Batch for the real destination and Simple with a console exporter for temporary debugging.
35Summary and Key Takeaways
A span processor decides when finished spans leave your program. The Simple Span Processor sends each span at once on the thread that ended it, which is easy to understand and ideal for learning and tests, but it ties your application’s speed to the network and the backend. The Batch Span Processor collects spans in a queue and sends them in groups from a background worker, which keeps your application fast and your network calls few, at the cost of some memory, a short delay, possible drops under overload, and the loss of queued spans on a hard crash. Both are only a step in the pipeline. Sampling decides which spans exist, the processor decides when to send them, and the exporter decides how and where.
Key Takeaways
- Simple exports every span immediately on the calling thread. Batch queues spans and exports them in groups from a background worker.
- Use Simple for tests, demos, and local debugging. Use Batch for any service with real traffic.
- Batch has four main settings: queue size, scheduled delay, maximum batch size, and export timeout. Defaults are usually a good start.
- When the queue is full, Batch drops new spans rather than slowing your application. Watch drops, queue size, and export time.
- Always call shutdown on graceful exit and flush at the end of short jobs and serverless invocations, or queued spans may vanish.
- A nearby Collector adds buffering and retries. Keep the SDK’s Batch processor and the Collector’s batch processor straight in your mind.
- Keep sensitive data out of spans, encrypt the path out, and use sampling to control volume before resizing queues.