Runtime coverage overhead of the beacon
Measured production overhead of the Fallow Cloud beacon per runtime and mode, compared with a baseline without the beacon, and the settings that lower the cost.
The beacon measures how often each function runs in production. Optional entry points also record error groups, branch-level usage and CPU time per function. This work has a cost in each process and each page that runs the beacon. This page gives the measured cost per runtime and mode, the source of each cost, and the settings that make it smaller.
Test conditions
All numbers on this page come from one benchmark run on 2026-10-27:
| Item | Value |
|---|---|
| Hardware | Apple M5, 10 cores, 32 GB RAM, macOS |
| Node | 22.23.3 (server and load client) |
| Bun | 1.4.2 |
| Browser | Headless Chromium 153 through Playwright 1.63.0, 4x CPU throttle |
| Instrumenter | oxc-coverage-instrument 0.18.0 |
| Beacon | @fallow-cli/beacon 0.6.6 |
Each mode ran 5 times. The runs were interleaved: every run executed each mode one time, and each run started one mode later. Each server run had a 5 s warm-up, a 20 s measured window, 32 keep-alive connections from a separate Node process, and a beacon snapshot every 5 s. Each browser run was a 20 s session in a new browser context.
Each cell is the median of the 5 runs. The range of the 5 runs is in brackets. When the ranges of two modes overlap, the difference is inside the noise of the machine.
Each runtime started only at a 1-minute load average of 2 or less (Node 1.89 after a 180 s wait, Bun 1.65, browser 1.85 after a 60 s wait). The 1-minute load average per run was 1.85 to 2.91 for Node, 1.61 to 3.37 for Bun and 1.09 to 1.85 for the browser. Use these numbers as a guide, and measure the effect on your own service before you decide. See How we measured.
The server target is a small JSON API with about 0.12 ms CPU per request. On this target, the relative cost is high. For a service with more work per request, the relative numbers below are an upper bound.
Node
The baseline has no beacon and no coverage.
| Mode | Throughput (req/s) | vs baseline | p99 latency (ms) | CPU per request (ms) | Snapshot block (ms) | Payload per snapshot (KB) |
|---|---|---|---|---|---|---|
| Baseline | 8481 [8159-8646] | 8.7 [8.4-9.1] | 0.118 [0.12-0.12] | none | none | |
| V8 | 5885 [5721-5929] | -31% | 12.0 [12.0-12.8] | 0.171 [0.17-0.18] | 10.0 [8.6-11.5] | 34.3 |
V8 + captureBlocks | 5916 [5792-5978] | -30% | 12.0 [11.8-12.3] | 0.171 [0.17-0.17] | 9.7 [8.7-11.8] | 67.0 |
| V8 + errors entry | 5841 [5654-5930] | -31% | 12.1 [11.9-12.6] | 0.173 [0.17-0.18] | 9.6 [8.3-10.0] | 43.7 |
| V8 + profiler window | 5853 [5639-5969] | -31% | 12.3 [11.9-12.6] | 0.173 [0.17-0.18] | 9.1 [8.1-9.3] | 39.2 |
oxc-coverage-instrument (Istanbul) | 7003 [6786-7339] | -17% | 10.3 [9.8-10.8] | 0.144 [0.14-0.15] | 4.8 [4.5-5.2] | 11.6 |
| Reference: binary V8 coverage, no beacon | 7807 [7674-8121] | -8% | 9.3 [8.8-9.4] | 0.129 [0.12-0.13] | 5.0 [3.6-5.5] | 16.1 |
The modes:
- V8: the process starts with
NODE_V8_COVERAGE, and the beacon runs in V8 mode. - V8 +
captureBlocks: as V8, plus branch-level usage. The throughput and CPU cost are inside the noise of V8. The payload per snapshot doubles. - V8 + errors entry: as V8, plus
withErrorCapturefrom@fallow-cli/beacon/errors, andcaptureErrorfor each failed request. The difference from V8 is inside the noise. - V8 + profiler window: as V8, plus
createCpuProfilerfrom@fallow-cli/beacon/profiler. The benchmark keeps one profiler window open for the full measured window, so this row is the cost while a window is open. The difference from V8 is inside the noise. - Istanbul: the target is built with
oxc-coverage-instrument, and the beacon reads the Istanbul counters. - Binary V8 coverage: this mode has no beacon. It starts precise coverage through
node:inspectorwithcallCount: falseanddetailed: false, so V8 records only if a function ran. It shows how much of the V8 cost comes from call counts and block ranges. Its throughput and CPU per request ranges do not overlap the baseline ranges, so this mode also has a measurable cost. The beacon does not use this mode.
Inside one snapshot, v8.takeCoverage() took 2.1 ms median [2.1-2.3]. The full snapshot (collect, diff, queue) blocked the event loop for 10.0 ms median in V8 mode and 4.8 ms in Istanbul mode. The peak RSS was 153 MB [152-154] for the baseline, 162 MB [162-165] for V8 and 159 MB [159-160] for Istanbul.
Bun
Bun does not implement v8.takeCoverage(), so Bun has no V8 mode. The Istanbul modes instrument the code at module load with a Bun preload that runs oxc-coverage-instrument.
| Mode | Throughput (req/s) | vs baseline | p99 latency (ms) | CPU per request (ms) | Peak RSS (MB) | Startup (ms) | Snapshot block (ms) |
|---|---|---|---|---|---|---|---|
| Baseline | 10581 [9953-10673] | 6.8 [6.7-7.8] | 0.098 [0.10-0.10] | 122 [117-135] | 15.3 [14.9-15.5] | none | |
| Istanbul | 9616 [8654-9756] | -9% | 7.4 [7.3-9.5] | 0.106 [0.10-0.12] | 148 [146-156] | 26.3 [26.3-35.6] | 0.20 [0.19-0.24] |
| Istanbul + errors entry | 9530 [9394-9612] | -10% | 7.5 [7.4-7.7] | 0.107 [0.11-0.11] | 158 [152-164] | 29.2 [27.0-39.3] | 0.21 [0.20-0.23] |
| Istanbul + profiler window | 9279 [9239-9332] | -12% | 7.7 [7.7-7.8] | 0.110 [0.11-0.11] | 232 [222-244] | 29.0 [28.6-42.3] | 0.21 [0.20-0.23] |
On Bun, the throughput and CPU per request ranges of each beacon mode do not overlap the baseline ranges. The p99 ranges overlap the baseline range. The startup time includes the instrumentation at module load. While a profiler window is open, the peak RSS is 232 MB against 148 MB for Istanbul alone. That is the clearest cost of the profiler on Bun. The payload per snapshot was 16.5 KB for Istanbul.
Browser
The browser target is a small shop page: a product grid with filters, sort, search and a cart. Vite built it two times: a plain build, and a build with Istanbul counters from oxc-coverage-instrument plus the browser beacon. Each session ran a fixed cycle of clicks and input for 20 s.
| Mode | JS bundle gzip (bytes) | Script at load (ms) | Script in session (ms) | LCP (ms) | INP (ms) | Ingest requests | Ingest bytes |
|---|---|---|---|---|---|---|---|
| Plain bundle | 1498 | 17.7 [16.0-19.7] | 786 [783-799] | 56 [52-60] | 16 | 0 | 0 |
Instrumented, sampleRate: 1.0 | 11923 | 18.7 [17.3-20.6] | 846 [843-858] | 60 [56-60] | 16 | 4 | 14093 |
Instrumented, sampleRate: 0.01 | 11923 | 19.2 [16.7-19.2] | 843 [827-849] | 60 [56-60] | 16 | 0 | 0 |
No session had a long task. The LCP median is 4 ms higher with instrumentation, but the ranges overlap, so the difference is inside the noise. INP is 16 ms in every mode. The app is small, so most of the bundle growth is the beacon and the coverage map. In your app, the counter cost grows with the amount of instrumented code that runs.
At sampleRate: 0.01, about 99 of 100 page loads do not start the beacon, so they send nothing. These page loads still download the larger bundle and run the coverage counters.
Bundle size
Each beacon entry point has a min+gzip size budget. The beacon build checks it with bun run size against size-budget.json in the beacon package source. You pay only for the entries that you import. The sizes below are min+gzip, measured on 2026-10-27.
| Entry | Import | min+gzip (bytes) |
|---|---|---|
| Node | @fallow-cli/beacon | 9190 (V8 path; a host with instrumented code loads about 8243) |
| Browser | @fallow-cli/beacon/browser | 6271 |
| Errors | @fallow-cli/beacon/errors | 4080 |
| Profiler | @fallow-cli/beacon/profiler | 7526 |
| Browser branch ranges | @fallow-cli/beacon/browser-blocks | 2608 |
| Browser performance | @fallow-cli/beacon/performance | 5796 |
| Error breakdown | @fallow-cli/beacon/errors-breakdown | 1664 |
| Script chunks | @fallow-cli/beacon/script-chunks | 292 |
Where the cost comes from
| Source | Where | What you pay |
|---|---|---|
| V8 precise coverage | Node with NODE_V8_COVERAGE | NODE_V8_COVERAGE starts precise coverage with call counts and block ranges for the whole process. V8 counts every call, so the cost is on each request, also between snapshots. On the benchmark this is the largest cost. |
| Istanbul counters | Instrumented code in Node, Bun and the browser | oxc-coverage-instrument adds a counter update to each function, statement and branch. The cost is on each request and each interaction, and it grows with the amount of instrumented code that runs. |
takeCoverage and the snapshot diff | Each snapshot | In V8 mode, v8.takeCoverage() is synchronous and blocks the event loop. The beacon then compares the snapshot with the last one and queues a payload. This was 10.0 ms per snapshot in V8 mode and 4.8 ms in Istanbul mode on Node, and 0.20 ms on Bun. |
| Payload upload | Each snapshot with new data | One POST /v1/ingest per snapshot. On the benchmark, each server mode sent 720 requests per hour at the 5 s interval. The size per snapshot is in the tables above. captureBlocks doubled it on Node. |
| Profiler window | Only while a window is open | The V8 CPU profiler samples at a 1 ms interval. The default is one 10 s window every 5 min. The benchmark kept one window open for the full measured time, so with the defaults the averaged cost is about 1/30 of the measured window cost. On Bun, the peak RSS grew while the window was open. |
| Instrumentation bytes | The browser bundle | Instrumented code is larger, and the coverage map and the beacon add bytes. Every page load downloads and parses them, also a page load that the sample rate skips. |
The benchmark took a snapshot every 5 s, so a short run had more than one snapshot. The default schedule takes one snapshot every 60 s. Scale the per-snapshot costs to your own interval: at 60 s, the beacon takes 60 snapshots per hour instead of 720.
Lower the cost
-
Sample browser sessions. Set
sampleRateto a value from 0 to 1. A skipped session sends nothing. The bundle bytes and the counters stay. -
Take fewer snapshots.
snapshotSchedulecontrols when the beacon takes a snapshot:"idle"(default): one snapshot every 60 s, on a timer that does not keep the process alive."interval": one snapshot everyflushIntervalMs(default 30 s)."manual": no automatic snapshot. You callflush(), for example after a batch job or before shutdown.
Fewer snapshots mean fewer event-loop blocks and fewer ingest requests. The per-request cost of the coverage counters does not change.
-
Leave the opt-ins off.
captureBlocks, the errors entry and the profiler are off by default. Turn on only the signal that you use.captureBlocksdoubled the Node payload. The profiler costs memory and CPU while a window is open. UseprofileWindowMsandprofileIntervalMsto change the window length and the time between windows. -
Pick the coverage source on Node. On this benchmark, the Istanbul mode cost less than the V8 mode:
Node mode Throughput vs baseline CPU per request Snapshot block Payload per snapshot V8 -31% 0.171 ms 10.0 ms 34.3 KB Istanbul -17% 0.144 ms 4.8 ms 11.6 KB The throughput ranges ([5721-5929] and [6786-7339]) and the CPU per request ranges of the two modes do not overlap. Istanbul needs a build step with
oxc-coverage-instrument, and it counts only the code that you instrument. V8 mode needs no build change, butNODE_V8_COVERAGEmust be set before the process starts. Measure both on your own service, because the Istanbul cost grows with the amount of instrumented code that runs.
How we measured
We compared each beacon setup, a mode, with a baseline without the beacon in the same run. A mode is one runtime with one beacon configuration, for example Node with NODE_V8_COVERAGE or Bun with Istanbul counters and error capture.
- Server. A small JSON API ran in Node and in Bun. A separate Node process sent load over 32 keep-alive connections. Each run had a 5 s warm-up and then a 20 s measured window. We measured throughput, p99 latency, CPU time per request, peak RSS, the time that each snapshot blocked the process, and the payload size per snapshot.
- Browser. A small shop page ran in headless Chromium with a 4x CPU throttle. Each run was a 20 s session in a new browser context. We measured the gzip size of the JavaScript bundle, the script time at load, LCP, INP and the number of ingest requests.
- Order. Each mode ran 5 times. The runs were interleaved: every run executed each mode one time, and each run started one mode later. So a slow period of the machine affected all modes, not one mode.
- Load gate. Each runtime started only at a 1-minute load average of 2 or less. We recorded the load average of each run, and the test conditions give the values.
- Results. Each cell is the median of the 5 runs, with the range of the runs in brackets. We compared each mode only with the baseline of the same runtime and the same run set.
The hardware and the versions are in the test conditions. The tables above list every measured mode.
See also
Set up the beacon in a browser or Node service and confirm the first ingest.
Use the runtime data in fallow health for hot paths and cold paths.
Read the runtime data of Fallow Cloud from an agent with get_cloud_runtime_context.
Upload the static inventory and source maps.