Delivery and flushing
The export queue, flush(), flushWithResult(), shutdown(), head sampling and quota back-pressure.
On this page
Spans are batched and exported over HTTP in the background, never inline with your request. That is what keeps recording off your latency budget — and what makes flushing necessary in any process that can end abruptly.
The queue
- Capacity
- 10 MB, including trace envelopes
- Batch size
- Up to 100 spans
- Interval
- Two seconds
- When full
- Oldest queued spans are dropped
The exporter retries transient failures, honours Retry-After, and splits
oversized batches. Dropping the oldest spans rather than blocking is deliberate:
a telemetry backlog must never become your application's backlog.
init() returns a client with stats() for queued and dropped counts.
flush()
import { flush } from "@tracehatch/sdk"
await flush()flush() waits for pending export with a bounded timeout — five seconds by
default — and never rejects. It also waits for runs whose streams are still
being read.
Call it before a serverless handler returns, before a script exits, and at the end of a queue job. Do not call it per request in a long-lived server: the batcher is already doing that job, and flushing per request gives up the batching.
flushWithResult()
import { flushWithResult } from "@tracehatch/sdk"
const result = await flushWithResult()
if (result.status !== "accepted") console.warn("Tracehatch delivery", result)Returns status, acceptedSpans, droppedSpans, pendingSpans and an
optional lastError: { code, httpStatus? } — a safe SDK or API code, never
request details or arbitrary server text.
| Status | Meaning |
|---|---|
disabled | No initialised client. Usually a missing or rejected key. |
empty | Nothing has been recorded yet for delivery. |
accepted | All queued work was acknowledged with no loss. |
timeout | Work is still pending when the timeout elapsed. |
failed | Something was lost. |
accepted requires the API's 202 receipt with a matching span count. It
proves ingestion accepted the batch; the project's trace view confirms it was
processed. A web page at the wrong address, a rejected key, a timeout and
disabled tracing each produce a distinct status rather than a false pass — which
is what makes this the right call for a
connection check.
shutdown()
import { shutdown } from "@tracehatch/sdk"
await shutdown()shutdown() flushes, stops the timers and removes the capture hooks. Reserve it
for process shutdown — calling it per request stops recording for everything
after it.
Automatic flushing at exit
With flushOnExit (the default), the queue is flushed on beforeExit and, with
a two-second bound, on SIGTERM and SIGINT. When nothing else handles the
signal, the SDK re-raises it afterwards so the process still exits. Open
automatic runs are ended first.
Vercel functions receive the final flush through waitUntil. On AWS Lambda,
Netlify and similar hosts, await flush() before the handler returns.
Head sampling
init({ sampling: { rate: 0.25 } })The decision is made per run when it starts, so spans of an unsampled run are never sent. That saves the work as well as the volume — and means an unsampled failure is invisible, because the outcome is not known yet when the decision is taken.
Quota back-pressure
While a workspace is over its monthly trace allowance, the API answers each
batch with a lower advisory sampling rate. The SDK applies it to new runs, logs
one warning, and returns to your configured rate as soon as the hint stops.
At three times the allowance the API rejects batches with 429 quota_exceeded
and a Retry-After. The SDK retries with backoff and eventually drops those
spans. Nothing in your application fails.
Server-side, above the allowance the ingest worker samples non-failed traces deterministically — failed traces are always kept — and sampled traces keep their counters and metrics but no span detail. See Plans, quotas and retention.
Errors never reach your code
A capture failure does not change the response or the error your application
receives. Export problems are logged by the SDK — with debug: true for the
detail — and reported through flushWithResult(). They are never thrown into
your request path.