Tracing FastAPI background tasks
Separate, linked traces for FastAPI background-task batches, so request and background work can be sampled independently.
A request can finish before its background work starts. That becomes an observability problem when tail sampling has already discarded the request trace: later spans can bypass its sampling decision and arrive with missing parents.
This contribution addresses the FastAPI part of that problem. The proposed change creates a separate root trace for each non-empty background-task batch, and links it to the original request span.
Linked work, independent sampling
A span link preserves the relationship without making the background batch a child of an already-finished request trace. The request and background work can then have independent keep-or-discard decisions. Callbacks within the same batch share one background-trace decision.

That separation also lets a background error retain earlier buffered records from its own trace. The request trace does not have to be retained for the background trace to remain useful. A link can legitimately point to a request span that sampling discarded.
Change the trace boundary, preserve execution
The wrapper runs the original task batch rather than replacing its callback machinery. Ordering remains unchanged, synchronous callbacks still use Starlette’s thread pool, and exceptions and cancellation continue to propagate.
token = context_api.attach(trace_api.set_span_in_context(trace_api.INVALID_SPAN))
try:
with self.logfire_instance.span('FastAPI background tasks', _links=[(self.request_context, None)]):
return await self.original_tasks()
finally:
context_api.detach(token)Clearing the active span starts a new root while keeping the surrounding telemetry context. The finally block restores the previous context. The patch preserves baggage, instrumentation suppression, and FastAPI’s own background-operation spans as children of the new trace.
Empty batches delegate directly to the original implementation; they do not create an extra trace.
Regression tests around the boundary
Coverage exercises all four combinations of retaining or discarding the request and background traces. It checks distinct trace IDs, request links, parent relationships, completed spans, background errors, synchronous and asynchronous callbacks, failures, cancellation, and context restoration.
| Reported local validation | Result |
|---|---|
| Focused background-task and tail-sampling tests | 35 passed |
| Broader relevant run before final test additions | 72 passed · 6 existing skips |
| Targeted fastapi.py coverage | 100% |
| Ruff checks, formatting, Linux-targeted Pyright | Passed |
These are the validation results reported in the PR, not a claim that the full upstream CI matrix has completed.
A deliberately narrow scope
The implementation covers FastAPI BackgroundTasks, including injected batches and directly returned responses carrying one. Plain Starlette background tasks and generic asyncio tasks remain outside this patch.

