Skip to content
Fallow home
All docs pages

Production CPU feedback for coding agents

Give a coding agent the production CPU time per function. The agent makes the slowest function faster and checks the result with verify_deployment.

A coding agent sees the code, but it does not see production. Fallow Cloud gives the agent feedback from production. It tells the agent which functions use the most CPU time. After the next deploy, it tells the agent if the change made them faster. This page gives the loop in four steps, with the numbers of one real run on the Fallow Cloud API.

Requirements

  • A beacon that sends production coverage. See Cloud beacon setup.
  • The CPU profiler next to the beacon (createCpuProfiler from @fallow-cli/beacon/profiler). See the beacon options.
  • The agent connected to the hosted MCP server. get_file_context and verify_deployment are only on the hosted server.
  • A Fallow Cloud organization with the continuous runtime coverage feature. See the tools.
  • Each deploy registers its commit SHA, so that Fallow Cloud can compare two deployments.

The loop

  1. Find the function with the most self CPU time

    Call get_cloud_runtime_context with state: "called". Each function has performance.cpu:

    {
      "state": "tracked",
      "self_ms_per_hour": 20535.26,
      "total_ms_per_hour": 24508.03,
      "share_of_process": 0.0643,
      "samples": 1314
    }
    • self_ms_per_hour is the CPU time inside the function itself, per hour of profiled wall time.
    • total_ms_per_hour adds the time of the functions it calls.
    • share_of_process is the part of the non-idle CPU samples of the process.
    • state is tracked, insufficient (too few samples) or untracked (no profile). Use only tracked values.

    Sort the functions on self_ms_per_hour and take the first one that your code controls. The time of native code (for example a database driver) counts as self time of the project function that calls it. A database call wrapper can be first only because the database is slow. To read one file, call get_file_context with path.

  2. Make it faster and measure it locally

    Change the function and open a pull request. Measure the change locally with a benchmark before and after. The production numbers come only after the deploy, so the local benchmark is the first check.

  3. Deploy and wait for profile windows

    The profiler records one window of 10 seconds every 5 minutes. verify_deployment compares CPU time when each deployment has enough windows and each side of the function has enough samples. On a deployment that ran for less than 6 hours, it needs 12 windows per deployment (about one hour) and 50 samples per side. Until then, the function has no slower or faster status.

  4. Verify the deployment

    Call verify_deployment with the SHA of the deploy and the function:

    {
      "repo": "my-api",
      "sha": "<commit SHA of the deploy>",
      "functions": ["src/db/client.ts:buildRow"]
    }

    The base is the previous deployment with production runtime. Set base to compare against another SHA. Each function gets a status: ran, slower, faster, errors_new, stopped_running, awaiting_runtime or not_yet_observed. The cpu block gives both sides.

    Verify the function that does the work after the change. If the change moves the work into a new function, the old function can stop running. It gets stopped_running when both deployments ran for 6 hours or more, else not_yet_observed. stopped_running sets the verdict to attention. Then read the CPU time of the new function with get_file_context and compare it with the old function on the base deployment.

How verify_deployment finds slower and faster functions

slower and faster compare the self CPU time per call, not per hour. The CPU time per hour also grows when the function gets more calls, with no change in the code. A night job that calls a function three times as often triples its CPU time per hour.

Self CPU time per call is the self CPU time per profiled hour divided by the production calls per observed hour. A function is slower or faster when:

  • both sides are tracked and both sides have production calls,
  • the CPU time per call changed by a factor of 1.5 or more,
  • the change per call, multiplied by the calls per hour of the new deployment, is 50 ms of CPU time per hour or more.

The cpu block gives these fields, among others:

FieldMeaning
headState, baseStatetracked, insufficient or untracked for each side.
headSelfMsPerHour, baseSelfMsPerHourSelf CPU time per profiled hour.
headCallsPerHour, baseCallsPerHourProduction calls per observed hour.
headSelfUsPerCall, baseSelfUsPerCallSelf CPU time per call, in microseconds. Null when that side has no production calls.
selfUsPerCallDeltaThe new value minus the base value.

The hosted MCP server gives these fields in snake_case.

The CPU time per call is an estimate. The profile windows stand for the full observed time. The cost per call also changes with the input of each call. A function that builds one result row per call costs more per call when the rows get wider.

A real run

The Fallow Cloud API runs the beacon and the profiler on itself. This is one pass through the loop.

  1. Find. On the base deployment, toRow in the database client had the most self CPU time of the project functions. It used 20,535 ms per hour, 6.4% of the process. The function builds one result object per database row, and it ran about 5.5 million times per hour.
  2. Change. The change computes the column plan one time per query and keeps one loop per row in a new function, buildRow. A local benchmark went from 760 to 520 ns per row.
  3. Deploy. The change went to production, and the profiler collected windows for almost 3 hours.
  4. Verify. The work moved from toRow to buildRow, so toRow stopped running and gets no CPU status. The comparison is toRow on the base deployment against buildRow on the new deployment.
Base (toRow)New (buildRow)
Self CPU time per hour20,535 ms8,266 ms
Production calls per hour5.52 million6.93 million
Self CPU time per call3.72 µs1.19 µs

The function got more calls per hour, and its CPU time per hour still fell by 60%. The CPU time per call fell by a factor of 3.1. The local benchmark showed a factor of about 1.5. The production value is an estimate. The rows of the two deployments were not the same. Thus, use the factor as a direction, not as an exact value.

In the same run, verify_deployment marked the database wrapper execute as faster. It went from 1,840 µs per call to 490 µs. That change does not come from this pull request. The self time of execute includes the native database driver, so it moves with the queries that the deployment ran. This is the case from step 1: a wrapper of native code is a poor target for the loop.

File context after each read (Claude Code hook)

In Claude Code, a PostToolUse hook can give the agent the production data of a file each time it reads or edits that file. The agent then sees the CPU time, the error groups and the branches that never ran before it changes the code. The hook calls GET /v1/coverage/:repo/file-context?format=text. That is the text form of get_file_context.

Save this script as fallow-file-context.mjs. It needs Node.js 18 or later and has no dependencies.

#!/usr/bin/env node
// Claude Code PostToolUse hook: adds the Fallow Cloud production context of a source file.
import { isAbsolute, relative, resolve, sep } from "node:path";

const SOURCE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/;
const SKIP = /(?:^|\/)(?:node_modules|dist|build|coverage|\.git)\//;

const main = async () => {
  const apiKey = process.env.FALLOW_API_KEY;
  const repo = process.env.FALLOW_REPO;
  if (!apiKey || !repo) return;
  let input = "";
  for await (const chunk of process.stdin) input += chunk;
  const event = JSON.parse(input);
  const filePath = event.tool_input?.file_path;
  if (typeof filePath !== "string") return;
  const root = process.env.FALLOW_REPO_ROOT ?? event.cwd ?? process.cwd();
  const path = relative(root, resolve(root, filePath)).split(sep).join("/");
  if (path.startsWith("../") || isAbsolute(path)) return;
  if (!SOURCE.test(path) || SKIP.test(path)) return;
  const api = (process.env.FALLOW_API_URL ?? "https://api.fallow.cloud").replace(/\/+$/, "");
  const query = new URLSearchParams({ path, format: "text", maxBytes: "4000" });
  const response = await fetch(
    `${api}/v1/coverage/${encodeURIComponent(repo)}/file-context?${query}`,
    {
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "x-fallow-accept-capture-policy": "positive_only",
      },
      signal: AbortSignal.timeout(2000),
    },
  );
  if (!response.ok) return;
  if (response.headers.get("x-fallow-file-state") !== "observed") return;
  const text = (await response.text()).trim();
  if (text.length === 0) return;
  const output = { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: text } };
  process.stdout.write(`${JSON.stringify(output)}\n`);
};

main()
  .catch(() => undefined)
  .finally(() => process.exit(0));

Add the hook to .claude/settings.json (project) or ~/.claude/settings.json (user):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Read|Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "node /path/to/fallow-file-context.mjs",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Set these variables in the environment of Claude Code:

VariableValue
FALLOW_API_KEYA read-only API key (fallow_read_k1_...). Required. A live key also works, but it can write data.
FALLOW_REPOThe repository as Fallow Cloud stores it, for example owner/repo. Required.
FALLOW_REPO_ROOTThe root of the repository. Set it when you start Claude Code in a subdirectory, for example a package of a monorepo. Default: the working directory of the session.
FALLOW_API_URLDefault: https://api.fallow.cloud.

How the hook works:

  • It sends only the repository-relative path of a source file. It skips files outside the repository, node_modules, build output and files that are not code.
  • It adds text only when the file has production data (the x-fallow-file-state: observed response header).
  • The server limits the text to the maxBytes value of the script (4000). Over the limit, the text leaves out the functions with the least production signal.
  • The server puts the text inside a notice line and two delimiter lines. The notice tells the agent to read the text as data, not as instructions.
  • The x-fallow-accept-capture-policy: positive_only header is necessary for a repository with a linked Vercel project. In that text, a function without a call is untracked, not never_called.
  • It stops after 2 seconds. On every error, it prints nothing and exits with code 0, so it never blocks the agent.

Because the hook is silent on an error, a wrong key or repository looks like a file without data. Test the key and the repository one time before you add the hook:

curl -s -H "Authorization: Bearer $FALLOW_API_KEY" \
  -H "x-fallow-accept-capture-policy: positive_only" \
  "https://api.fallow.cloud/v1/coverage/$(node -p 'encodeURIComponent(process.env.FALLOW_REPO)')/file-context?path=src/index.ts&format=text"

A text block tells you that the key and the repository are correct. A JSON error has a code that tells you what is wrong.

Context only

The verify result is advice for the agent or for a CI comment. It does not prove that a change is correct or safe. A confirmed verdict means that the functions ran with no new error group, no CPU regression that the gates can show, and no function that stopped running.

See also