Why Is My Coding Agent Slow?

Necco Ceresani
Necco Ceresani
·24 min read

Fifteen years building engineering platforms, currently focused on advanced AI infrastructure at yeet. I love turning the deeply complex topics into something everyone can understand. I relate deeply with the core yeet philosophy that you can just build things.

Last updated: October 2026

Quick answer. A single agent turn is at least six serialized waits: context gathering (thousands of openat calls across your repo), subprocess spawns for every shell command, a language server or type checker the edit triggered, MCP server round-trips, TLS setup plus time-to-first-token to the provider, and the token generation everyone blames. Only the last is the model. Before switching models or upgrading hardware, measure which wait you actually have: yeet run github:yeet-src/claudefeed -- --only=exec,conn shows the local work with timestamps, and procs { schedstat { run_delay } } in the yeet system graph tells you whether the machine is making your agent queue for a CPU.

I build eBPF tooling at yeet, where thousands of engineers point these scripts at their own machines, and agent latency is the complaint I get asked about that has the least data behind it. I cannot tell you what your provider's inference queue is doing, and nobody outside it can. What I can tell you is how much of a slow turn never left your laptop, which turns out to be the part people have not measured and the only part they can change. The gap between "the model is slow today" and "this turn spent eleven seconds walking node_modules" is one observation pass wide.

Why is there a long pause before my agent's first token appears?

Because a turn is a serialized pipeline and the model is one stage in it. When you send a message, the agent gathers context (reading files, listing directories, sometimes searching the tree), sends a request, waits for the first token, streams the rest, then executes whatever tool calls came back, which means spawning processes and reading more files, and often loops through that two or three more times before answering. Wall clock is the sum. A provider serving tokens at full speed cannot compensate for four seconds of local filesystem work that happened before the request was built.

The reason this misattributes so reliably is that the model is the only stage with a visible progress indicator. You watch tokens appear, so that is the part you experience as the wait, and the seconds before the first token look like one undifferentiated pause. Anything that happens in that pause (a directory walk, a cold language server, an MCP server starting up) is invisible unless you go looking, and it is invisible in exactly the way that makes people conclude the model got slower.

There is a second reason worth naming, because it explains why the same agent feels different on two machines. Most of the local stages are latency-bound rather than throughput-bound: a thousand sequential openat calls with a slow answer each is dominated by per-call latency, not by disk bandwidth or core count. That is why a faster CPU often changes nothing and why a repository on a network filesystem or an overlay mount can make an agent feel broken while every system metric looks healthy.

Which of the six stages in an agent turn is actually the bottleneck?

Six stages, and they are worth laying out in order because each has a different fix and only one of them is the model. The illustrative numbers below are for a single mid-sized turn on a repository with a large dependency tree, to make the shape concrete rather than to report a benchmark.

StageWhat is happeningTypical fixVisible in the model's response time
Context gatheringThousands of openat and stat calls building the promptIgnore files, narrower scope, avoid network mountsNo
Subprocess spawnsexecve per shell command, plus its childrenFewer, larger commands; warm cachesNo
Language server or type checkerAn edit triggers a project-wide re-checkIncremental mode, exclude generated codeNo
MCP server round-tripsJSON-RPC to a local subprocess, or HTTPS to a remote oneDrop unused servers; prefer localNo
TLS setup and time-to-first-tokenHandshake, queueing, prompt processing at the providerPrompt caching, smaller context, closer regionPartly
Token generationThe model producing outputA faster model, shorter outputsYes

Two things to take from the table. Five of the six stages are invisible in any latency number the provider reports, so a dashboard showing healthy inference times is consistent with a slow agent. And the top four stages all live on your machine, which means they are the ones you can measure directly and the ones a kernel-side tool can attribute.

How do I profile one agent turn with timestamps on Linux?

Watch the local stages from the kernel with timestamps, then subtract. The whole method is to record what the agent's process tree does during one turn, note the gap between the last local event and the first response, and attribute the remainder to the provider. Nothing more sophisticated is needed to answer the question people actually have.

Start with the event feed, which gives you execs, file opens and connections for the session subtree:

curl -fsSL https://yeet.cx | sh                                  # install the yeet daemon, once
yeet run github:yeet-src/claudefeed -- --secs=120 > turn.log     # one bounded observation window

Send one message to your agent inside that window and let the turn complete. --secs=120 bounds the run so it exits on its own, and because the feed is append-only with one line per event and no full-screen repaint, redirecting it produces a clean grep-able log. Flags go after -- so the runtime routes them to the script instead of consuming them itself.

Now read the log for three things. The count of open lines is your context-gathering cost, and it is usually the number that surprises people; a turn that opens several thousand files is doing work you can remove. The exec lines and their timestamps show subprocess spawns and how long the tree stayed busy after each one. The conn lines mark when the agent actually reached the provider, so the gap between your last local event and that connection is local overhead, and the gap after it is the provider's.

For the exec side specifically, the folded view is easier to read than a flat feed:

yeet run gh:yeet-src/exectop -- --pid $$     # then launch your agent from this shell

exectop follows one application's process tree through fork in the kernel and collapses repetition into one row per kind, so a turn that spawned 400 processes reads as a handful of rows with counts. That compression is what makes the outlier visible: a single unexpected process is one more line in a flat log and an obvious row in a folded one. The README's illustration is a build where 425 execs fold into rows led by echo ×124 and date ×90.

Is my agent queued for a CPU or blocked on disk? Check run_delay

Ask the scheduler, because it keeps the one number that separates those two cases and almost nobody reads it. Run delay is time a process was runnable but not running, meaning it had work to do and was queued behind something else for a core. A high run delay says the machine is oversubscribed and your agent is waiting for CPU; a low run delay with a slow turn says the agent is blocked on something else, which is usually I/O or a network round-trip.

The yeet system graph exposes it per process alongscheduler and memory counters, so one query answers the question:

yeet graph query '{
  load_average { one five fifteen }
  kernel_stats { procs_running procs_blocked }
  procs {
    pid
    stat { comm state utime stime rss_bytes }
    schedstat { sum_exec_runtime run_delay pcount }
  }
}'

Read run_delay against sum_exec_runtime for the same process: a process that has accumulated substantial delay relative to the time it actually ran is queueing rather than working. procs_blocked from kernel_stats is the companion number, counting processes blocked on I/O right now, and state per process tells you which ones are in uninterruptible sleep. Between those three you can distinguish the three ordinary causes without guessing: CPU contention shows as run delay, I/O waiting shows as blocked processes and D states, and neither one showing up during a slow turn points at the network.

This matters because the two cases have opposite fixes and the wrong one is expensive. CPU contention on a laptop is usually a type checker, a test watcher, or an indexer running concurrently, and closing it is free. I/O blocking is usually the repository's location (a network mount, an overlay filesystem, an encrypted volume) and no amount of CPU helps. The deeper version of this diagnosis is covered in how to find which process is slowing your machine on Linux, including what voluntary and nonvoluntary context switches tell you, and this post does not re-answer it.

Why is my agent reading node_modules for a one-file question?

Because it has to decide what is relevant before it can ask, and without a good index the cheapest available strategy is to look. Reading a file is how an agent learns whether that file matters, so a question about one function can turn into a traversal of the directory containing it, then its imports, then a search across the tree. Every one of those is an openat and a read, and the cost is per-call latency multiplied by a count you never see.

Three things drive the count up, in the order they usually bite:

  1. Dependency directories that were never excluded. node_modules, vendor, .venv, target and build contain enormous numbers of files that are almost never the answer. An agent that walks them is spending most of its context budget and most of your wait on generated code. Ignore files are the fix and they are underused because the cost is invisible.
  2. The repository living somewhere slow. The same traversal over NFS, SMB, a synced folder, or a Docker bind mount on a non-Linux host pays a network or translation round-trip per call. This is the single largest local factor I see, and the tell is a high open count with low CPU and low run delay.
  3. Broad questions. "Why is this failing" over a large repository is a search; "why is this failing in src/auth/session.ts" is a read. Naming the file is not a courtesy to the agent, it is the difference between a bounded read set and an unbounded one.

Counting the opens is what turns this into a fix, and that is one line off the log you already captured:

grep -c ' open ' turn.log                        # how many files this turn touched
grep ' open ' turn.log | grep -c node_modules    # how many of them were dependencies

If the second number is a large fraction of the first, you have found your slow turn and the fix is a config file rather than a faster model.

Does moving my repo off an NFS or Docker bind mount speed up my agent?

Usually yes, and it is the one hardware-shaped change that reliably pays, because it changes per-call latency instead of throughput. An agent turn is thousands of sequential file operations, and on NFS, SMB, a synced folder, or a Docker bind mount from a non-Linux host every one of them pays a network hop or a translation layer. Multiply a few hundred microseconds of added latency by four thousand opens and the arithmetic gives you seconds per turn, spent entirely before the request is built.

The tell is specific enough to check in one pass: a high open count with low CPU utilization and near-zero run delay. That combination means the agent is neither computing nor queueing for a core, so it is waiting on the filesystem, and the filesystem is the thing to move. Copy the checkout to local disk and re-run the same measurement; if the open count is unchanged and the turn is faster, the mount was the cost.

For the general question of whether faster hardware fixes a slow machine, including whether a faster CPU helps a coding agent specifically, find which process is slowing your machine on Linux answers it directly and this post does not repeat it. The short version for a turn that is already measured: if run_delay is near zero, the machine is not the constraint and neither is its price.

How much latency does each configured MCP server add per turn?

More than most people expect, because each configured server is a process in the path and its cost is paid whether you use it or not. A local stdio server is launched as a subprocess of your agent, per the MCP specification, which means it has a startup cost at session start and a round-trip cost on every tool call. A remote server over the Streamable HTTP transport adds a network round-trip per call, and each message on that transport is an HTTP POST, so the latency is a request rather than a pipe write.

The measurement is the same feed with a different lens. Servers appear as exec lines at session start and as their own connections and file opens afterward, so watching the subtree tells you which servers started, how long each took, and which ones did anything during the turn:

yeet run github:yeet-src/claudefeed -- --only=exec,conn --secs=60

A server that appears at startup and never appears again during a working session is pure overhead, and removing it from your configuration is the cheapest latency win available. Servers that did participate are worth knowing about for a different reason: what they open and dial while servicing a call is not in your agent's transcript at all, which is a separate question covered in how to audit a local MCP server on Linux.

Which fix returns the most: ignore files, local storage or a faster model?

Five changes, ranked by what they return for the effort, and the ranking holds for most setups because it follows the stage list rather than taste:

  1. Exclude dependency and build directories. Largest single win when the open count is high, and it is a config file edit. This removes work rather than speeding it up, which is why it beats everything below it.
  2. Move the repository to local storage. If the checkout is on a network mount, a synced folder, or a bind mount from a non-Linux host, this changes per-call latency for every stage that touches a file, which is most of them.
  3. Remove MCP servers you do not use. Each one costs startup time and configuration surface for zero benefit if no tool call reaches it. Check the feed for servers that start and never participate.
  4. Name files in your requests. A bounded read set is the difference between a read and a search, and it is free. This is a habit rather than a setting, which is why it gets skipped despite working.
  5. Then consider the model. A faster model or a shorter output shortens exactly one stage of six. It is a real improvement and it is the last thing to reach for, because the four items above are cheaper and often larger.

When should I use perf or strace -c instead?

When the question stops being about your machine, which is a different problem with different tools. Four cases where the approach here is the wrong shape:

  • You need the provider's side of the number. Time-to-first-token, queueing and prompt-processing time live at the provider, and nothing on your host can attribute them. What a kernel probe gives you is the boundary: when the connection opened and when the response finished. Everything inside that interval is inference plus network, undifferentiated.
  • You are profiling a program rather than diagnosing a wait. If the slow thing is a build or a test suite the agent triggered, profile that program with the tools built for it (perf, a language profiler, strace -c for a syscall histogram) instead of watching the tree from outside.
  • You need history or fleet-wide comparison. These tools retain nothing; the record is what you piped to a file. Comparing agent latency across a team over weeks is an APM or a metrics pipeline, and it always was.
  • The agent runs in CI. A CI runner's slowness is usually cold caches and image pulls, which the job's own timing output already exposes at better granularity than a process-tree feed.

The bottom line: measure the four local stages before changing the model

If your agent feels slow, count the file opens in one turn first, because that number is both the most common cause and the easiest to fix: yeet run github:yeet-src/claudefeed -- --secs=120 > turn.log, then grep -c ' open ' turn.log. If a large share of those opens are in node_modules, vendor or .venv, the fix is an ignore file and not a faster model. If the open count is modest and the turn is still slow, ask the scheduler with yeet graph query and read run_delay: high means the machine is oversubscribed and something else on the box should be closed, near zero means the machine is not your constraint and hardware will not help. If the wait is in subprocess spawns, exectop folds them into rows and ranks the unusual ones, which is how you find the process you did not know your agent was running. If MCP servers start and never participate, remove them. And if all four local stages are clean and the gap sits between the connection opening and the response arriving, the wait really is the provider, which is the one case where changing models is the right response. Reaching for that first is how teams spend a model upgrade on an ignore-file problem.

The part worth carrying is that five of the six stages in an agent turn are invisible to every latency number anyone reports, and four of those five are on your own machine.

Frequently asked questions

Why is my AI coding agent slow even though the model is fast?

Because a turn is a serialized pipeline and inference is one stage. Context gathering across thousands of file reads, subprocess spawns, a language server the edit triggered, and MCP round-trips all happen before or after the model call, and none appear in a provider's latency numbers. A model serving tokens at full speed cannot offset local filesystem work that happened before the request existed.

How do I measure how long an AI agent spends reading files?

Watch the agent's process subtree from the kernel and count the open events for one turn. A probe on the openat tracepoint gated on the session's process ids records every path the agent and its children opened, with timestamps, so the count and the elapsed span are both directly readable. claudefeed does this and its output is a plain append-only log you can grep.

What is run delay on Linux and why does it matter for agent latency?

Run delay is time a process was runnable but not running, meaning it had work and was queued behind something else for a CPU. It is the number that separates CPU contention from every other cause of slowness: substantial run delay means the machine is oversubscribed, while near-zero run delay during a slow operation means the wait is I/O or network and adding cores will not help.

Does a Docker bind mount slow down file reads on macOS or Windows?

Yes, measurably, because the mount crosses a virtualization boundary and every file operation pays that translation. For a workload of a few thousand sequential opens, which is what one agent turn looks like, the per-call cost compounds into seconds. The diagnostic signature is a high open count with low CPU utilization and near-zero scheduler run delay.

What is a normal number of file opens for one agent turn?

There is no published baseline, which is why the useful measurement is relative rather than absolute: capture one turn, then capture another after a change and compare. What matters is the share landing in directories that cannot contain the answer, since that fraction is pure waste and is the part a configuration change removes.

How do I tell whether my agent is waiting on the network or on my disk?

Compare two numbers during a slow turn: the count of blocked processes from the kernel's stats, and the timestamps around the connection to the provider. Blocked processes and uninterruptible sleep states point at I/O; a long gap between the last local event and the response with nothing blocked points at the network or the provider.

Do MCP servers add latency to an agent turn?

Yes, in two places. A local stdio server is a subprocess the client launches, so it costs startup time at session start and a round-trip on each tool call. A remote server over Streamable HTTP adds a network round-trip per call, because each message is an HTTP POST. A configured server that never participates in a turn is pure overhead worth removing.

Can I see time-to-first-token from my own machine?

Only as a boundary, not as a breakdown. A kernel probe records when the connection to the provider was initiated and your client records when output arrived, so the interval between them is available, but everything inside it (queueing, prompt processing, inference, network transit) is undifferentiated from the outside. Splitting that interval requires instrumentation at the provider.

Why does my agent feel slower on a repository in a Docker bind mount?

Because every file operation pays a translation or network round-trip, and an agent's context gathering is thousands of sequential file operations. Bind mounts from a non-Linux host, network filesystems and synced folders all multiply per-call latency, and the resulting slowness shows up as a high file-open count with low CPU utilization and low run delay.

What is the lowest-overhead way to trace what a process and its children do on Linux?

Kernel probes gated on a process subtree, because the filtering happens before userspace exists and the traced process is never stopped. Unlike ptrace-based tools such as strace, which impose a stop on every traced syscall, an eBPF probe drops out-of-scope events after one map lookup, so the cost scales with the watched subtree's activity instead of the machine's total syscall volume.

Does watching an agent with eBPF slow it down further?

Not meaningfully, which is what makes measuring a latency problem with it reasonable. The probes are passive observers with no stop imposed on the traced process, and an in-kernel map gate means events outside the watched subtree never cross into userspace. Dropping the noisiest event class narrows it further when only commands and connections matter.

Should I use a profiler instead of tracing the process tree?

Use a profiler when the slow thing is one program, such as a build or a test suite the agent triggered, because a profiler attributes time inside that program's own code. Trace the process tree when the question is where a multi-stage wait went across several processes, which is what an agent turn is. They answer different questions and a slow turn often needs both in sequence.

Sources

  • claudefeed (yeet-src, GitHub) — the five event classes (exec, exit, open, conn, listen); the tracked tgid hash map that gates the file and network probes in the kernel so a system-wide openat firehose never reaches userspace, making the cost scale with the watched session's activity; the self-propagating tracked set across exec; the append-only one-line-per-event output that is safe to pipe with colors no-op off a TTY; and the --secs, --only and --except flags.
  • claudefeed: what you can't see (yeet-src, GitHub) — that open events carry paths and access modes but never contents; the openat-only coverage excluding open(2), openat2 and mmap, so an open count is a floor rather than a total; that exec fires at syscall entry so requested rather than successful execs are recorded; TCP-only connection coverage with UDP unhooked; and that nothing is retained between runs.
  • exectop (yeet-src, GitHub) — the scoped process-launch monitor on the sched_process_exec, sched_process_fork and sched_process_exit tracepoints, following one application's tree through fork in the kernel; selection by launch, container or pid; and the folding that turns 425 execs into a handful of rows led by echo ×124 and date ×90, which is what makes a single unexpected command legible as an outlier.
  • proc(5) (Linux man-pages project) — the definitions behind the fields this post reads: /proc/[pid]/schedstat carrying time spent on the CPU, time spent waiting on a run queue, and the number of timeslices run; /proc/[pid]/stat carrying utime, stime and the process state character where D is uninterruptible sleep; and /proc/stat's procs_running and procs_blocked counters for runnable and I/O-blocked process counts.
  • CFS scheduler documentation (kernel.org, Linux kernel documentation) — the scheduler design that makes run-queue waiting time a meaningful signal, and the per-task statistics exposed through schedstat when scheduler statistics are enabled. This is the grounding for reading run delay against actual execution time to separate CPU contention from I/O and network waits.
  • stdio transport (Model Context Protocol specification, 2026-07-28) — that "the client launches the MCP server as a subprocess" communicating over standard streams with newline-delimited JSON-RPC, which is why a local server costs process startup at session start and a pipe round-trip per tool call, and why servers appear as exec events in a process-subtree feed at all.
  • Transports overview (Model Context Protocol specification, 2026-07-28) — the definition of the Streamable HTTP transport as "each message is an HTTP POST to a single MCP endpoint," with replies as a JSON object or a request-scoped SSE stream. That framing is the reason a remote MCP server adds a network round-trip per tool call rather than the pipe write a local server costs.
  • perf (Linux perf wiki) — the sampling profiler this post routes readers to when the slow component is one program such as a build or test suite the agent triggered, rather than a multi-process wait spanning an agent turn. It attributes time inside a program's own code, which a process-tree event feed deliberately does not attempt.
  • yeet (yeet) — the JS runtime both scripts are written in, the daemon that performs the privileged BPF load so neither needs a setuid binary, and the GraphQL system graph queried here for load_average, kernel_stats, and per-process stat and schedstat including run_delay and sum_exec_runtime. yeet graph dump prints the full schema for probing other fields.

Related resources


Built with yeet, a JS runtime for writing eBPF programs on Linux machines. Join us on discord.