Skip to content
Fallow home
All docs pages

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:

ItemValue
HardwareApple M5, 10 cores, 32 GB RAM, macOS
Node22.23.3 (server and load client)
Bun1.4.2
BrowserHeadless Chromium 153 through Playwright 1.63.0, 4x CPU throttle
Instrumenteroxc-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.

ModeThroughput (req/s)vs baselinep99 latency (ms)CPU per request (ms)Snapshot block (ms)Payload per snapshot (KB)
Baseline8481 [8159-8646]8.7 [8.4-9.1]0.118 [0.12-0.12]nonenone
V85885 [5721-5929]-31%12.0 [12.0-12.8]0.171 [0.17-0.18]10.0 [8.6-11.5]34.3
V8 + captureBlocks5916 [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 entry5841 [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 window5853 [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 beacon7807 [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 withErrorCapture from @fallow-cli/beacon/errors, and captureError for each failed request. The difference from V8 is inside the noise.
  • V8 + profiler window: as V8, plus createCpuProfiler from @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:inspector with callCount: false and detailed: 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.

ModeThroughput (req/s)vs baselinep99 latency (ms)CPU per request (ms)Peak RSS (MB)Startup (ms)Snapshot block (ms)
Baseline10581 [9953-10673]6.8 [6.7-7.8]0.098 [0.10-0.10]122 [117-135]15.3 [14.9-15.5]none
Istanbul9616 [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 entry9530 [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 window9279 [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.

ModeJS bundle gzip (bytes)Script at load (ms)Script in session (ms)LCP (ms)INP (ms)Ingest requestsIngest bytes
Plain bundle149817.7 [16.0-19.7]786 [783-799]56 [52-60]1600
Instrumented, sampleRate: 1.01192318.7 [17.3-20.6]846 [843-858]60 [56-60]16414093
Instrumented, sampleRate: 0.011192319.2 [16.7-19.2]843 [827-849]60 [56-60]1600

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.

EntryImportmin+gzip (bytes)
Node@fallow-cli/beacon9190 (V8 path; a host with instrumented code loads about 8243)
Browser@fallow-cli/beacon/browser6271
Errors@fallow-cli/beacon/errors4080
Profiler@fallow-cli/beacon/profiler7526
Browser branch ranges@fallow-cli/beacon/browser-blocks2608
Browser performance@fallow-cli/beacon/performance5796
Error breakdown@fallow-cli/beacon/errors-breakdown1664
Script chunks@fallow-cli/beacon/script-chunks292

Where the cost comes from

SourceWhereWhat you pay
V8 precise coverageNode with NODE_V8_COVERAGENODE_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 countersInstrumented code in Node, Bun and the browseroxc-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 diffEach snapshotIn 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 uploadEach snapshot with new dataOne 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 windowOnly while a window is openThe 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 bytesThe browser bundleInstrumented 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 sampleRate to a value from 0 to 1. A skipped session sends nothing. The bundle bytes and the counters stay.

  • Take fewer snapshots. snapshotSchedule controls 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 every flushIntervalMs (default 30 s).
    • "manual": no automatic snapshot. You call flush(), 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. captureBlocks doubled the Node payload. The profiler costs memory and CPU while a window is open. Use profileWindowMs and profileIntervalMs to 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 modeThroughput vs baselineCPU per requestSnapshot blockPayload per snapshot
    V8-31%0.171 ms10.0 ms34.3 KB
    Istanbul-17%0.144 ms4.8 ms11.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, but NODE_V8_COVERAGE must 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