Skip to content

Application performance analysis with Claude Code is a measure-first loop run from the terminal: the agent records a reproducible baseline, runs a real profiler or EXPLAIN ANALYZE, ranks hotspots with evidence, and changes one bottleneck at a time. A benchmark budget written before the fix, not the agent’s own account, decides whether the change is faster.

This page is for the developer who owns a service that got slow, and for the tech lead who wants performance fixes that arrive with proof. The orders API answered in about 120 ms last quarter and now takes almost two seconds. No single commit explains it, and extra servers did not help. Ask an agent to “make it faster” and it rewrites the code that looks slow, which is rarely the code that is slow.

  • A permission setup that lets Claude Code run profilers and benchmarks, but not edit the benchmark it is judged by
  • A baseline-and-budget file that fixes the pass bar before any code changes
  • Profiler commands for Node.js, Python, and PostgreSQL, and a prompt that turns their output into a ranked, cited hotspot list
  • Five copy-paste prompts: baseline, profile, query fix, leak hunt, and fix-and-prove
  • A table of the evidence that proves a speedup without you reading the whole diff
  • The failure modes that make agent-driven optimization lie, and how to recover from each

Why does an agent need a baseline before it optimizes?

Section titled “Why does an agent need a baseline before it optimizes?”

A model reading source code sees patterns: a nested loop, a SELECT *, a synchronous call. It cannot see which of them run ten thousand times per request and which run once at startup. Static review produces a plausible list of “optimizations” that often misses the real cost, and every one of them adds diff you have to trust.

A profiler and a query plan replace the guess with a measurement. So the working rule on this page is: no fix without a measured baseline, a profile that names the hotspot, and a pass bar written down before the change. Claude Code is good at the tedious parts of that loop: writing the harness, running it many times, reading a 40,000-line profile, and comparing the numbers. You keep the one judgment the agent should not make alone: what counts as fast enough.

Profiling needs shell access: benchmark scripts, node --cpu-prof, py-spy, and psql. Allow the benchmark and profiler commands by name, approve psql, pgrep, and kill case by case, and deny edits to the folder that holds the benchmark and its budget. An agent that can rewrite its own benchmark can pass it without getting faster.

.claude/settings.json
{
"permissions": {
"allow": [
"Bash(node perf/bench.mjs *)",
"Bash(node perf/compare.mjs *)",
"Bash(node --cpu-prof *)",
"Bash(py-spy record *)",
"Bash(npm test *)"
],
"deny": [
"Edit(perf/**)"
]
}
}

Edit(perf/**) blocks the file tools only. A shell command can still write to perf/, so the durable protection is in CI: the gate reads the budget from the base branch, and changes to perf/ need a code owner’s approval. See Claude Code configuration for how permission rules match.

Start the investigation in plan mode (/plan, or Shift+Tab until the mode shows plan). Claude then reads code and proposes a plan before it edits anything. Switch modes only when you have a ranked hotspot list and agree with it.

  1. Pick the three slowest user-facing paths. Take them from your monitoring, not from intuition. If you have no monitoring yet, start with the endpoints users complain about and set up monitoring with Claude Code afterwards.

  2. Ask Claude Code to build a reproducible benchmark. It runs against a local or staging copy with production-shaped data. Ten rows in a table hide every N+1 query.

  3. Write the target into perf/budget.json yourself. For example, /api/orders p95 ≤ 300 ms. A target you set before seeing the fix cannot drift to match whatever the fix delivered.

  4. Seed the data at production scale. If the spread from step 2 is wide, fix the environment first: close other workloads, pin the Node.js version, and use the same data snapshot for every run.

Give Claude Code a profiler’s output, not a vague symptom. The commands below produce files the agent can read. Ask it to summarize them with a script, because a raw CPU profile or heap snapshot is far larger than you want in the context window.

Server entry point (add near the top)
// Exit cleanly on SIGTERM so Node.js writes the CPU profile before it stops
process.on('SIGTERM', () => process.exit(0));
Terminal window
# Terminal: write a V8 CPU profile while the benchmark hits the server
node --cpu-prof --cpu-prof-dir=perf/profiles dist/server.js &
node perf/bench.mjs --endpoint /api/orders --runs 1
kill %1 # SIGTERM: the handler above exits cleanly, so the .cpuprofile is written

--cpu-prof writes the profile when Node.js exits normally. Without a SIGTERM handler, kill ends the process without writing it and the perf/profiles/ folder stays empty. If your server already has a graceful-shutdown handler, make sure it ends with process.exit() instead of adding a second one.

If three endpoints are slow for unrelated reasons, ask Claude to investigate them in parallel with one subagent each. Each subagent returns only its ranked list, so the main conversation stays small. See custom subagents to make a reusable perf-investigator with read-only tools.

Fix slow queries with a plan before and after

Section titled “Fix slow queries with a plan before and after”

Database access is the usual first suspect on a CRUD service, but a profile has to confirm it. When the profile points at query time, the two classic causes are an N+1 pattern and a missing index. Both show up clearly in EXPLAIN ANALYZE: many small identical queries, or a sequential scan over a large table.

For repeated database work you can give Claude Code direct, read-only access with Postgres MCP Pro (PyPI postgres-mcp, community project by Crystal DBA). Its restricted mode uses read-only transactions and execution-time limits, and it adds index-tuning and plan tools. Its last PyPI release was 0.3.0 on 2025-05-16 (checked 2026-09-26), so pin that version and check the repository for activity before you rely on it. Connect with a read-only database role as well: restricted mode is a second fence, not the only one.

Terminal window
# Terminal: add the server for this project only (local scope), read-only
claude mcp add postgres -e DATABASE_URI="$STAGING_DATABASE_URL" -- uvx postgres-mcp==0.3.0 --access-mode=restricted

Your shell expands $STAGING_DATABASE_URL before Claude Code sees it, so the connection string is saved in your local, per-user MCP configuration rather than in the repository. Never add this server with --scope project, which writes .mcp.json into the commit.

psql through the Bash tool works equally well for one-off plans. For more database servers and their read-only switches, see database MCP servers.

Two details decide whether this is safe. EXPLAIN ANALYZE executes the statement, so for an UPDATE or DELETE wrap it in BEGIN; … ROLLBACK;. And PostgreSQL refuses CREATE INDEX CONCURRENTLY inside a transaction block, which is how most migration tools run each file by default. For schema changes at scale, continue with database and code migrations and database work with Claude Code.

A leak shows up as memory that climbs across identical requests and never returns after garbage collection. Reading the source for leak patterns can generate suspects, but two heap snapshots taken minutes apart show what actually grew.

Terminal window
# Terminal: take a heap snapshot on demand
node --heapsnapshot-signal=SIGUSR2 dist/server.js &
kill -USR2 %1 # snapshot 1, after warm-up
node perf/bench.mjs --endpoint /api/dashboard --runs 3
kill -USR2 %1 # snapshot 2, after load

The same budget idea works for the browser. Ask Claude Code to add rollup-plugin-visualizer (Vite and Rollup) for a treemap, and size-limit for a check that fails the build when a chunk grows past its limit. Set the limit to today’s size first, then lower it as fixes land. The ratchet gives the agent a target and gives CI a gate. Lazy-loading every route can create a waterfall of small requests, so tell the agent how many chunks may sit on the critical path.

Work in a separate worktree so the benchmark on your main checkout stays a clean control. claude -w perf-orders creates a git worktree and starts the session inside it.

Then make the gate independent of the agent. perf/compare.mjs is a deterministic script that CI can run on every merge to the main branch. When it fails, a headless Claude Code run can explain the regression without write access:

Terminal window
# CI or a nightly job on the trusted main branch, after compare.mjs fails
claude -p "perf/compare.mjs failed. Read perf/latest.json, perf/budget.json and git log -10. \
Name the most likely commit and hotspot, citing the numbers. Do not change files." \
--permission-mode dontAsk \
--allowedTools "Read" "Grep" "Bash(git log *)" "Bash(node perf/compare.mjs *)" \
--max-budget-usd 2 --output-format json > perf/regression-report.json

claude -p starts in Manual mode (auto mode on third-party providers or with telemetry off, from v2.1.285), so --permission-mode dontAsk plus an exact --allowedTools list keeps the run to those tools plus reads in the working directories and read-only commands. Run it only on trusted refs, never on code from an untrusted pull request with secrets in the environment. See Claude Code in CI/CD for the workflow file and its permissions.

Avoid putting the benchmark in a PostToolUse hook. It runs for minutes, fires after every edit, and teaches the agent to tune for noise. Run it once, at the end of the task, where the prompt above puts it.

How do you know a performance fix is real?

Section titled “How do you know a performance fix is real?”

You do not need to read every line of the diff to trust the speedup. You need these five pieces of evidence, attached to the pull request as part of its evidence bundle:

ClaimEvidence that proves itWho checks it
It is fastercompare.mjs output: baseline, new p95, and spread, with the gain larger than twice the spreadCI gate
It is still correctThe existing test suite passes, plus a test for any query the fix rewroteCI gate
The right thing was fixedThe profile or plan from before the fix names the same file and function the diff changesReviewer, in two minutes
Nothing else got slowerEvery endpoint in budget.json passes, not only the targetCI gate
The bar did not moveperf/budget.json unchanged, or changed in a separate, code-owner-approved commitCode owner

The reviewer’s job shrinks to two questions: does the profile justify this change, and is the budget untouched? The author signs off on the evidence and the code owner signs off on any budget change. For a load profile beyond a single-machine benchmark, run the load and stress tests before release.

What breaks when an agent optimizes performance?

Section titled “What breaks when an agent optimizes performance?”

The agent optimized code that was never hot. The diff is tidy, the tests pass, and p95 has not moved. Recovery: revert, and re-run the profile prompt. Accept only fixes whose hotspot the profile names, and reject anything marked “unmeasured”.

The win is smaller than the noise. Two runs differ by 20% before any change, so a 10% “improvement” means nothing. Recovery: use the spread from baseline.json. Run more iterations, stop background workloads, and use a dedicated CI runner for the gate.

The benchmark was gamed. The agent added a cache keyed on the benchmark’s exact request, lowered the connection count, or edited the budget. Recovery: keep Edit(perf/**) in the deny list, have CI read the budget from the base branch, and vary the request parameters in bench.mjs.

The N+1 fix made a worse query. One JOIN across five tables can cost more than five indexed lookups on large result sets. Recovery: compare the EXPLAIN (ANALYZE, BUFFERS) output before and after, and ask Claude for the batched IN (...) version as an alternative.

The index migration failed or locked writes. CREATE INDEX CONCURRENTLY cannot run inside a transaction block, and a plain CREATE INDEX blocks writes on the table while it builds. Recovery: move the index into its own non-transactional migration. If a concurrent build fails, it leaves an invalid index behind: drop it and run the migration again.

The session drowned in a profile. The agent read a raw .cpuprofile or heap snapshot, the context filled up, and answers got vague. Recovery: /rewind to before the read, and ask for a summarizing script instead. Claude Code performance optimization covers context limits in depth.

The instrumentation became the slowdown. Timing wrappers on every function add overhead of their own. Recovery: put ad-hoc timing behind an environment flag, and use sampling profilers such as --cpu-prof and py-spy, which do not change your code.