
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
openatcalls 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,connshows the local work with timestamps, andprocs { 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.
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.
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.
| Stage | What is happening | Typical fix | Visible in the model's response time |
|---|---|---|---|
| Context gathering | Thousands of openat and stat calls building the prompt | Ignore files, narrower scope, avoid network mounts | No |
| Subprocess spawns | execve per shell command, plus its children | Fewer, larger commands; warm caches | No |
| Language server or type checker | An edit triggers a project-wide re-check | Incremental mode, exclude generated code | No |
| MCP server round-trips | JSON-RPC to a local subprocess, or HTTPS to a remote one | Drop unused servers; prefer local | No |
| TLS setup and time-to-first-token | Handshake, queueing, prompt processing at the provider | Prompt caching, smaller context, closer region | Partly |
| Token generation | The model producing output | A faster model, shorter outputs | Yes |
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.
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.
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.
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:
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.open count with low CPU and low run delay.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.
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.
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.
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:
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:
perf, a language profiler, strace -c for a syscall histogram) instead of watching the tree from outside.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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.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.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/[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.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.exec events in a process-subtree feed at all.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.schedstat, stat and the procs_blocked counter this post reads.Built with yeet, a JS runtime for writing eBPF programs on Linux machines. Join us on discord.