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.
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.
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
kindderived 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.
Added 2026-07-01. Back to the Skill Library.

Get Smarter About AI Dev
New tutorials, open-source projects, and deep dives on coding agents - delivered weekly.