Profiler
The continuous profiler samples where your app spends its CPU, maps it back to your source files, and shows the hot paths per version. Pro, off by default. Read it with `runlot profile`, in the dashboard, or from an AI agent.
When a route is slow or the app uses a whole core, the question is which of your functions takes the time. The continuous profiler answers it from production: it samples a CPU profile of your app for 10 seconds about every 10 minutes while it serves requests, maps each frame back to your source with the deployment's source maps, and keeps the result per deployment version and, where it can tell, per route.
From those samples you get:
- the top functions by self time, the time spent in the function itself;
- the flame graph, all windows merged into one tree of call paths;
- a comparison between two versions, each function's share of self time in both.
Pro, and off by default
The profiler is on the Pro plan only, and it stays off until you turn it on for a project:
runlot profile onProfiler setting for site: on · effective: on
It applies at the next deploy or restart of the app; nothing restarts the app for it.The setting takes effect when the app next starts: at your next deploy, or the next time the app restarts. Runlot does not restart a running app to attach the profiler or to remove it. runlot profile off turns it off the same way, and runlot profile default returns the project to the default (off). Changing the setting needs an organization admin.
What it costs. With the profiler on, the app process used about 11% more CPU over time in our measurement, and no request was measurably slower. The extra CPU is the profiler's connection to the process and the sampling, so it is paid by the process, not by the request. Process CPU counts toward your plan's CPU. Profiled time is billed at $0.03 an hour (see Settings).
Profiles are kept twice as long as the plan keeps events.
runlot profile
runlot profile [status] [dir] # the setting, the plan, and windows in the last 24 hours
runlot profile on | off | default # turn it on or off (admin, Pro)
runlot profile windows # the stored 10-second windows
runlot profile top # functions by self time
runlot profile compare <a> <b> # two deployment versions side by side
runlot profile flame # the flame graph's hot paths as textEvery read takes --json, which prints the API's answer unchanged. When there is nothing to show, the command says why in one line: the profiler is off, the plan does not include it, or no window was taken in that time yet.
| Option | What it does |
|---|---|
--since 24h | The start of the window: an age (6h, 3d) or a UTC time. The default is the last 24 hours; for compare, all the profiles still kept. |
--from <time> --to <time> | A window between two times instead. A time without a zone is UTC. |
--version <n> | One deployment version. runlot versions lists them. |
--route <route> | One route template, such as "GET /api/items/:id". |
--limit <n> | How many rows windows, top and compare print (top and compare default to 20). |
--min-share <f> | flame: fold paths under this share of all samples, as 0.02 or 2% (the default). |
--depth <n> | flame: follow paths at most this many frames deep. |
Status
runlot profilesite · continuous profiler on
Setting on
Effective on — what the next deploy or restart launches with
Plan pro
Windows 138 in the last 24 h, newest 2026-11-09T11:50:00Z
Read it with `runlot profile top`, `runlot profile flame` or `runlot profile compare <a> <b>`.Setting is what you chose, or default. Effective is what the app gets at its next start; it is off on a plan without the profiler, whatever the setting says.
Top functions
runlot profile top --version 42Top functions by self time · 12,400 samples · 2026-11-08T12:00:00Z → 2026-11-09T12:00:00Z · version 42
SELF% SELF TOTAL FUNCTION
31.2% 3,869 5,102 renderRow (src/table.ts)
12.0% 1,488 1,488 JSON.stringify
6.4% 794 9,850 handle (src/server.ts)
SELF is samples taken in the function itself; TOTAL includes what it called.SELF counts the samples taken inside the function itself, and TOTAL also counts the functions it called. A function's lines add up into one row.
The flame graph as text
runlot profile flame --version 42Flame summary · 12,400 samples · 2026-11-08T12:00:00Z → 2026-11-09T12:00:00Z · version 42
Paths with at least 2.0% of samples; percentages are of all 12,400 samples.
TOTAL% SELF% FRAME
100.0% 0.0% (root)
90.0% 0.0% dispatch (node_modules/hono/dist/hono.js:211:9)
… 6 frames, one path …
90.0% 6.4% handle (src/server.ts:10:3)
60.0% 31.2% renderRow (src/table.ts:20:5)
25.0% 20.1% escape (src/html.ts:4:2)
20.0% 12.0% JSON.stringify
(3 smaller paths, 3.0%)
7.0% 7.0% (garbage collector)Each line is a frame with its share of all samples: TOTAL% with everything it called, SELF% in the frame itself. Indentation is the call path. To keep it readable in a terminal, and by an AI agent:
- paths under
--min-shareare folded into one(N smaller paths, X%)line under their parent, so the numbers still add up; - a run of frames that only pass the call along, such as a framework's dispatch, is shown as its first and last frame;
- the output stays within 80 lines, and keeps the hottest paths.
The summary says so when the graph was too large and was cut on the server (what did not fit is under (other)), and when no frame could be mapped to your source, so the positions are in the deployed bundle. --json prints the whole graph, including each frame's position in the bundle.
Comparing two versions
runlot profile compare 41 42Version 41 → 42 · self-time share by function · 9,800 / 12,400 samples · 2026-10-26T00:00:00Z → 2026-11-09T12:00:00Z
v41 v42 CHANGE FUNCTION
10.4% 31.2% +20.8 pp renderRow (src/table.ts)
14.0% 6.0% −8.0 pp parse (src/parse.ts)
Shares of each version's own samples, so versions profiled for different lengths of time compare.Each version is measured as a share of its own samples, because two versions are never profiled for the same length of time. The largest changes come first, in percentage points.
Routes
--route keeps the samples taken while every request in flight was for that one route. Your app runs on one thread but switches between requests at every await, so a sample taken while requests for two routes were open belongs to neither; it still counts in the whole-app views. A route with little traffic of its own may therefore have few samples even when it is slow. Use the whole-app view, or --version, when a route has none.
From an AI agent
The MCP server has the tool runlot_profile_summary. It returns one text: the top functions and the flame summary above, for a version, a route and a time window. It is a read, so it is available under runlot mcp --read-only.
Ask your agent to call it when a route is slow, when get_project_health reports a CPU risk, and after a deploy, once per version, to see what changed. The profiler has to be on, and it has data only after the app was deployed or restarted with it on and then served requests.
The API is under /v1/orgs/{org}/projects/{project}/observability/profiles (top, flamegraph, compare) and /observability/profiler for the setting.
Heap snapshots
Memory is not sampled. With the profiler on, an admin can take a heap snapshot of the running app from the dashboard. The app pauses while it is taken, so it is one at a time and at most one every 10 minutes. See Settings.