Metrics
Built-in request, memory, infrastructure and database metrics, custom metrics from env.metrics, and metrics derived from your logs.
Built-in metrics
Collected for every project, with nothing to turn on.
| Section | Metrics |
|---|---|
| Overview | Requests, errors, status classes, wall time p50/p95/p99, with deploy and OOM markers |
| Infrastructure | Memory against the limit, CPU usage and throttling, pressure stall (PSI) for CPU, memory and I/O, I/O bytes, process starts and exits by cause, park and wake with wake latency, Durable Objects per class |
| Database | env.db statement fingerprints, database size and growth, table and index sizes |
Memory is read from your project's own cgroup, so the chart is your process against your limit — never a neighbour's, never the node's. Inside the last 24 hours it has 10-second points; up to 15 days, one a minute; beyond that, one an hour. When memory reaches the soft limit and the process is slowed rather than killed, the chart marks it throttled.
Metrics can be read back 15 days on Free and 15 months on Pro.
Database
Observability → Database groups env.db statements by fingerprint: literals become $n and IN lists collapse, so SELECT * FROM orders WHERE id = 8812 and … id = 9001 are one row. Per fingerprint: calls, total and mean time, p95 and p99, errors by SQLSTATE, and the routes that issued it, over the last 48 hours. The sample statement is the normalised shape; literals never leave the node. Query monitoring is Pro.
The health tab lists the database size and its history, tables and indexes, and pgvector tombstones waiting for the next park-time VACUUM. It runs fixed catalog statements, never your SQL, and needs the member role and db:read.
Custom metrics
env.metrics is on every handler's env. It needs no binding in runlot.json.
export default {
async fetch(request, env) {
const started = Date.now();
const order = await placeOrder(request, env);
env.metrics.count("orders.placed", 1, { plan: order.plan });
env.metrics.distribution("orders.cents", order.cents, { plan: order.plan });
env.metrics.gauge("queue.depth", order.queueDepth);
env.metrics.distribution("checkout.ms", Date.now() - started);
return Response.json(order);
},
};| Method | Records |
|---|---|
count(name, value = 1, tags) | A count added up per interval |
gauge(name, value, tags) | The last value |
distribution(name, value, tags) | Every value, so p50 to p999 are computed over any range, not averaged from hourly figures |
Code with no env at hand imports the same three functions:
import { count, distribution } from "runlot:metrics";
count("cache.miss");-
A name starts with a letter or
_and holds letters, digits,_ . : - /, up to 200 characters. Up to 16 tags a point; values are turned into strings. -
Points are never sampled, so a metric is exact even when logs are not.
-
The tags in
observability.tagsare added to every point:runlot.json { "observability": { "tags": { "team": "payments" } } }Up to 16 tags. A key is lower case and cannot be one the platform sets:
org,project,env,version,commit,handler,route,region,binding,peer.host,service,do.class. -
If your project already has a binding called
metrics, it keeps the name and the facade isenv.RUNLOT_METRICS. -
Custom metrics ride on Runlot Errors' in-isolate capture. With
"observability": { "errors": false },env.metricsis not defined and therunlot:metricsfunctions do nothing. -
runlot devprints each point to the console as[runlot:metrics].
Series
A series is one metric name with one combination of tag values. Each project gets 100 distinct series a month on Free and 2,000 on Pro. Past that, a new series is refused and counted, and the series you already have keep recording; Observability → Metrics shows the refusals. On Pro you can raise or remove the cap, and series past 2,000 are billed at $0.05 per series per month. A tag whose values are unbounded — a user ID, a request ID — creates a series per value; put those in a log field instead.
Metrics from logs
A log-to-metric is a count or a distribution defined by a filter over your log events or invocation records. It is computed at ingest before sampling and before the daily cap, so it stays exact while stored logs are sampled at 1 %.
{
"logMetrics": [
{
"name": "checkout.failed",
"kind": "count",
"dataset": "events",
"filter": [{ "key": "event", "op": "eq", "value": "checkout_failed" }],
"groupBy": ["reason"]
},
{
"name": "slow_requests",
"kind": "distribution",
"dataset": "invocations",
"filter": [{ "key": "$wall_ms", "op": "gt", "value": 1000 }],
"valueKey": "$wall_ms",
"groupBy": ["$route"]
}
]
}Each one becomes the custom metric log.<name>, each group-by key becomes a tag, and its series count toward the series allowance like any other. A name is lower case, starts with a letter and holds letters, digits, _ and ., up to 64 characters. Up to 50 per project. Define them in the file above (see Pipelines) or under Settings → Log metrics.