Skip to main content
Watch: Claude Code Just Killed After Effects

Deploy & Ops

Request Tracing with Spans

Use when adding lightweight per-request observability to an API, especially an LLM or media route, so you can see where the time went, which model ran, and what the request cost - without a heavyweight tracing vendor. Also use when a route is intermittently slow and nothing records which step was responsible. Covers timed spans, one durable row per request, and best-effort writes that never throw into the caller. If latency or cost needs attributing, use this skill.

observabilitytracingspansneonlatency

1 file

Description

Use when adding lightweight per-request observability to an API, especially an LLM or media route, so you can see where the time went, which model ran, and what the request cost - without a heavyweight tracing vendor. Also use when a route is intermittently slow and nothing records which step was responsible. Covers timed spans, one durable row per request, and best-effort writes that never throw into the caller. If latency or cost needs attributing, use this skill.

Install
SKILL.md

name: request-tracing-spans description: Use when adding lightweight per-request observability to an API (especially LLM or media routes) so you can see where time went, which model ran, and what a request cost, without a heavyweight tracing vendor. Covers the span/waterfall pattern, best-effort persistence, and per-thread grouping.

Request Tracing with Spans

When to trigger

A route does several timed steps (auth, a model call, a storage write) and you want a durable record of each step's duration, the model used, and the credits spent, grouped by conversation so a UI can chart per-thread timelines.

One tracer per request, one row at the end

Construct a tracer at the start, time named sub-steps with span(), record the model and credits as you go, then write exactly one row on end(). A span stores its start offset from the request start plus its duration, which is all a waterfall needs.

const t = startTrace({ clerkId, threadId, route, method });
const reply = await t.span("model", () => streamText({ model, messages }));
t.setModel(model);
t.addCredits(cost);
await t.end(); // writes one request_traces row

The span helper records the span whether the inner function resolves or throws (record in finally, then rethrow), so a failed step still shows up in the waterfall.

Instrumentation must never break the request

Every write is best-effort: a failed insert is swallowed, never thrown into the caller. Tracing that can crash the request it measures is worse than no tracing. Wrap all persistence so the worst case is a missing row, not a 500.

Group by thread, key by owner

  • Carry a threadId (the logical conversation or session) on every trace so the UI can roll up per-thread stats and render a timeline.
  • Bucket by a kind derived from the route prefix (voice, image, upload, chat, other) so daily activity charts by family.
  • Key every read helper on the caller's id so a user only ever sees their own traces.

Pitfalls

  • Letting a trace insert throw couples your observability to your uptime. Make writes best-effort and swallow their errors.
  • Storing absolute wall-clock timestamps per span makes the waterfall math awkward. Store the start offset from the request start plus the duration.
  • Recording the span only on success hides exactly the slow, failing steps you most want to see. Record in finally.
  • Forgetting to scope reads by owner leaks one user's request timings to another. Key every query on the caller id.
AI Development Stack

Get Smarter About AI Dev

New tutorials, open-source projects, and deep dives on coding agents - delivered weekly.

One email per weekReal code, not theoryFree forever