The short version
- A
202 Acceptedresponse means the batch is stored. There’s no background job to wait for. - Resending a span with the same
trace_idandspan_idreplaces it. It never creates a duplicate. - Trace totals (duration, LLM time, counts, tokens, cost, status) are recomputed from the stored spans after every request, so retries and multi-batch traces never double-count.
- An optional
Idempotency-Keyheader makes an exact retry return the original response without reprocessing anything. - Retry network errors and
5xxresponses with backoff. Don’t retry400or401until you’ve fixed the request.
The Idempotency-Key header
Do I need it?
Not for correctness. Because spans are stored by(trace_id, span_id), a retry without a key rewrites the same rows with the same values and the totals come out identical. The key is still worth sending because:
- A replay is cheaper: nothing is reprocessed.
- It protects the trace-level fields. Without a key, a stale retry that arrives after a newer batch rewrites
root_name,environment,sourceandtagswith the stale values (spans and totals stay correct either way).
Concurrent duplicates
The key is recorded after the batch is stored, so two copies of the same request that arrive at the same moment may both be processed. That’s harmless for the reasons above: both write the same spans, and the totals are restated from storage.Response
Every successful ingest returns202 Accepted with the standard envelope:
Despite the
202, ingestion is synchronous. The spans and the recomputed totals are written before the response is sent, so a GET of the trace right after returns the new data.
Out-of-order and partial delivery
You don’t have to send a trace in one piece or in order:- Children before parents. A span’s
parent_span_idisn’t checked on arrival. The tree is assembled from the stored spans when the trace is displayed, so a parent that arrives later is linked in. - Batches in any order. The trace’s
started_atis always the earliest stored span start andduration_msthe full extent, whichever batch arrives first. - Errors are sticky. Once any span of a trace is stored with
status: "ERROR", the trace staysERROR. A later batch of successful spans can’t clear it, and neither can resending the trace with a trace-levelstatus: "OK". To clear it, resend the failed span itself withstatus: "OK". - Last write wins per span. If two versions of the same
span_idarrive, whichever is stored last is kept, in full. - Last write wins for trace labels.
root_name,environment,sourceandtagstake the values from the most recent batch. Send the same values on every batch.
Retry guidance
Use exponential backoff with jitter, cap the number of attempts, and keep the body and
Idempotency-Key identical across attempts. For example:
Throughput and limits
Each request is processed synchronously, so a large backlog is faster sent with a few requests in parallel than one at a time. Keep the concurrency modest (a handful of requests in flight), and don’t let tracing block your agent’s own work: send traces from a background task or queue, and drop or buffer them if AgentHub is unreachable rather than failing the user’s request.
OpenTelemetry exports
The OTLP endpoint (POST /api/v1/traces) doesn’t use Idempotency-Key, because OpenTelemetry exporters have no such concept. It relies on the same span-level deduplication instead: a retried export rewrites the same spans. See OpenTelemetry (OTLP).