Traces and APM
Spans from every binding with no tracer to install, per-route requests, errors and latency, the service map, deployment comparison and trace retention filters.
Turn traces on in runlot.json, or in the dashboard's Settings:
{
"observability": {
"traces": { "enabled": true, "head_sampling_rate": 0.1 }
}
}head_sampling_rate is the share of invocations whose spans are kept, greater than 0 and at most 1; omitted, it is 1. The decision is made once per request, when it arrives.
What becomes a span
You install nothing. env.db, env.storage, env.ai, queues, env.email, Durable Object calls and outbound fetch all go through Runlot's own services, and those services record their spans themselves — an env.db span carries the statement with its literals removed, the row count and the time. The trace ID is the request ID.
Traces join across services through the W3C traceparent header. An inbound traceparent becomes the parent of the request's spans, so a trace that started in your own system continues into Runlot.
Routes
Everything in APM is grouped by route template — /users/:id/orders, not /users/8812/orders — so one route is one row. A template comes from the first of these that has it:
- Your framework's route table, read from the deployed bundle (Next.js, React Router, SvelteKit, Astro, Nuxt).
- Templates you declare in
runlot.json. - Learned templates: numbers, UUIDs, hex and long tokens become
:id, and a path position that sees more than 100 distinct values in an hour becomes a parameter.
{
"observability": {
"routes": ["/users/:id/orders", "/files/*"]
}
}A declared template starts with /, and each segment is a literal, a :name, or a final *. Up to 500. A framework's table that does not match the traffic — under 80 % of paths covered after 1,000 requests, or one template catching more than half of them — is set aside for the learned tier. Observability → Traces shows which tier each route came from; you can pin a learned template, or reject one so it is never applied.
Routes, breakdown and service map
| View | What it shows | Plan |
|---|---|---|
| Routes | Requests, errors and wall time p50 to p999, per route and handler | Free: the top 5 routes. Pro: all |
| Latency breakdown | A route's wall time split by span kind — your own code, env.db, outbound fetch by host, storage, AI, Durable Objects — so "this route got slow" says because of what | Pro |
| Service map | Your project, its bindings, Durable Object classes, the projects it calls and the external hosts it reaches, with each edge's rate, error rate and p95. Drawn from spans, so it is what ran, not what was declared | Pro |
| Deployment comparison | Two versions side by side: per-route requests, errors and latency, the breakdown, memory peak, and error groups first seen in the newer one | Pro |
Traffic from your synthetic checks is left out of these by default.
The trace explorer
The trace explorer lists traces newest first and filters by route, status (502, or a class such as 5xx), minimum duration, errors only and version. Open one for its waterfall, with the request's logs beside it.
runlot logs request 4bf92f3577b34da6a3ce929d0e0e4736 # the same waterfall, in the terminalHow long traces are kept
Spans are kept as long as the plan keeps logs: 3 days on Free, 7 on Pro. On Pro, a trace is kept 30 days when it
- contains an error,
- is in the slowest 1 % of its route, or
- matches one of your retention filters.
A retention filter names a route, a handler, a status (error, 5xx or 502), a minimum duration in milliseconds, or span attributes; every criterion given must match. Add them under Settings, or with POST …/observability/trace-filters. Up to 20 per project. Bytes kept past the normal retention are billed at $0.10 per GB-month.
Spans count toward a daily allowance of 200,000 on Free and 2,000,000 on Pro per project. Past it, counts are kept and successful detail drops to 1 %; traces with errors are still kept.