Skip to main content
Networks fail, workers restart, and queues redeliver. The ingest endpoint is built so that sending the same data twice, or sending it in the wrong order, never corrupts a trace. This page explains the guarantees and how to write a client that relies on them.

The short version

  • A 202 Accepted response means the batch is stored. There’s no background job to wait for.
  • Resending a span with the same trace_id and span_id replaces 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-Key header makes an exact retry return the original response without reprocessing anything.
  • Retry network errors and 5xx responses with backoff. Don’t retry 400 or 401 until you’ve fixed the request.

The Idempotency-Key header

Never reuse an idempotency key for different data. If you send batch 2 of a trace with batch 1’s key, you get batch 1’s response back and batch 2 is silently dropped. Give each distinct batch its own key.

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, source and tags with 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 returns 202 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_id isn’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_at is always the earliest stored span start and duration_ms the full extent, whichever batch arrives first.
  • Errors are sticky. Once any span of a trace is stored with status: "ERROR", the trace stays ERROR. A later batch of successful spans can’t clear it, and neither can resending the trace with a trace-level status: "OK". To clear it, resend the failed span itself with status: "OK".
  • Last write wins per span. If two versions of the same span_id arrive, whichever is stored last is kept, in full.
  • Last write wins for trace labels. root_name, environment, source and tags take 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).