Skip to content
runlot
ObservabilityMetrics

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.

SectionMetrics
OverviewRequests, errors, status classes, wall time p50/p95/p99, with deploy and OOM markers
InfrastructureMemory 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
Databaseenv.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);
  },
};
MethodRecords
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.tags are 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 is env.RUNLOT_METRICS.

  • Custom metrics ride on Runlot Errors' in-isolate capture. With "observability": { "errors": false }, env.metrics is not defined and the runlot:metrics functions do nothing.

  • runlot dev prints 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 %.

.runlot/logs.json
{
  "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.

On this page