flushBySize (flush_by_size in Python) adds a third competing trigger: flush the buffer when the next span would push it past a byte limit.
Default batching
Two triggers, whichever fires first, plus the timeout the resulting request gets:
Lowering the span count is the usual first move when exports are too large, and it is often enough. It cannot bound the payload though: 20 spans carrying a 2 MB payload each is a 40 MB export no matter how low the count is set, and if the count goes low enough to fix that, small spans start exporting one request at a time.
When to flush by payload size
The byte trigger earns its place when spans are large and many of them finish at once. One agent on its own rarely gets there: model calls are slow, so spans trickle in and the scheduled delay fires long before the buffer grows. Running the same work in parallel is what changes that. The usual case is an evaluation over a dataset of images, PDFs, or long documents on your own machine, where dozens of concurrent tasks each end a multi-megabyte span within the same few seconds. Whichever of the two triggers fires first then hands the exporter a buffer whose size nothing has looked at. Symptoms, if you are already hitting it:- Exports rejected as too large by the ingest endpoint, so every span in that batch is lost. See Troubleshooting for the exact errors per transport.
DEADLINE_EXCEEDEDon export, because a single request is big enough to outlast the export timeout.
Enable it
- TypeScript
- Python
@lmnr-ai/lmnr 0.8.45 or later.Options
The byte limit on its own does nothing. Without
flushBySize, maxExportBatchSizeBytes is ignored and the plain batch processor is used, so enabling one without the other is a no-op rather than a silent change of behavior.
disableBatch wins over flushBySize. With batching off, every span is exported on its own and there is no buffer to bound.
What the limit measures
The size of a batch is estimated, not computed from the encoded request. The SDK adds up the UTF-8 byte lengths of span names, attribute keys, string attribute values, event and link attributes, and status messages. Numbers and booleans are counted as a flat 8 bytes, and protobuf framing, ids, and timestamps are not counted at all.Treat the limit as a target, not a guarantee. The estimate runs below the real encoded size, and in Python the flush is handed to a background thread, so spans that end while it runs join the batch on their way out. Pick a value with headroom under the largest request your endpoint accepts rather than one exactly at it.
Without the flag all 12 spans fit under the span count limit, so they leave in one request. With it, the buffer is flushed each time the next span would cross 4 MiB. Those numbers are from the TypeScript SDK; Python splits the same spans into three exports, one of them over the limit, because its flush is asynchronous.
Flushing is still your job at exit
The byte trigger only bounds how large a batch gets. It does not flush the buffer when your process ends, so short-lived scripts, serverless handlers, and CLI tools still need an explicit flush. See Flushing and shutdown.Self-hosted deployments
If you self-host, you control the ingest limit as well as the batch size. The app server readsHTTP_PAYLOAD_LIMIT (default 5 MiB) and GRPC_PAYLOAD_LIMIT (default 25 MiB), both in bytes. The two settings are complementary: the server limit is what rejects a request, the batch size is what keeps you from building one.
Both server defaults are below the SDK’s 32 MiB default, so on a self-hosted deployment set maxExportBatchSizeBytes under whichever limit applies to your transport (gRPC unless you pass forceHttp), or raise the server limit to match.
What’s next
Flushing and shutdown
Send pending spans before a script, Lambda, or worker exits.
SDK lifecycle
flush, force_flush, and shutdown reference for both SDKs.TypeScript initialization
Every option
Laminar.initialize() accepts in TypeScript.Python initialization
Every option
Laminar.initialize() accepts in Python.