I caught an agent scheduling a twenty-minute wait between two passes of a task it was ready to run immediately — the failure I later started counting daily as a "smoke break". Nothing was blocking it. No rate limit, no build in flight, no input needed from me. It just... paced itself.
My reaction was less measured than what follows. But the underlying issue is real and it's worth naming properly, because it's one of the least-discussed sources of wasted wall-clock time in agentic work.
Fixed idle waits are pure latency
The maths is unforgiving. If an agent checks a condition every N seconds, its worst-case response time is N seconds, and its average is N/2 — regardless of how fast the underlying work completes.
polling every 1200s, work finishes at t=30s
-> detected at t=1200. 1,170 seconds of nothing.
event-driven, work finishes at t=30s
-> detected at t=30. 0 seconds of nothing.
Published comparisons put event-driven triggers 70–90% ahead of polling on responsiveness, and that matches what I see. The gap isn't a tuning problem you fix by shortening the interval — a shorter interval just trades idle latency for wasted checks. The gap is architectural: polling asks "is it done yet?", events say "it's done."
And it compounds badly in a chain. Five sequential steps, each paced on a modest interval, and a task whose real work is two minutes takes an hour. Nothing failed. Nobody was blocked. The whole cost was scheduling.
The rule
Simple version: when the next step is ready, run it now.
The only legitimate reason to wait is a real external blocker — something that will actually reject or invalidate the call if you make it now:
- A rate-limit window that will genuinely return a 429. Wait exactly as long as the header tells you to, not a round number you picked.
- A build or deploy in flight where the thing you'd check doesn't exist yet.
- Something only a human can supply — a credential, an approval, a decision. Then stop and ask, rather than looping.
- External state you genuinely cannot subscribe to — a CI run, a remote queue. Here polling is correct, and the interval should be derived from how long that thing actually takes. An eight-minute CI run deserves one check at eight minutes, not eight checks at one minute.
Everything else is a smoke break. And note the asymmetry: an agent that acts too eagerly wastes a cheap tool call, while an agent that paces unnecessarily wastes the thing you can't buy back.
The deferral version, which is worse
The timer is the honest form of this failure. The dishonest form is verbal:
- "I'll do that as a focused pass later."
- "That needs verification first, so I'll come back to it."
- "Here's why I won't do that right now."
Each of these is a task silently leaving the queue while sounding like project management. The killer is that they read as diligence. A sentence about sequencing feels more professional than just doing the fourth thing on the list, so it doesn't get challenged — and the item never comes back.
The rule I now hold: when several tasks are queued, do every one to completion, in order. If something is genuinely blocked, say so in one line and keep moving on the rest. Partial delivery with a clear statement of what's outstanding beats a tidy explanation of why nothing happened.
Scaling the work down is the requester's call, not the agent's.
The money pattern: make progress visible from the first unit
Here's the related failure that taught me the most, because nothing was technically broken.
A long job was designed to write its output at the end of each unit of work, and the largest unit was queued first. So I sat watching an empty output for a long stretch, with no way to distinguish "working correctly" from "hung". I restarted it several times for fixes, and each restart binned in-flight work.
The code was fine. The observability was the bug — and for the human watching, invisible is indistinguishable from broken.
# BAD — one visible write, at the end, biggest unit first
for unit in sorted(units, key=size, reverse=True):
results = [process(x) for x in unit]
write_all(results) # nothing appears until here
# GOOD — write per item, smallest unit first
for unit in sorted(units, key=size):
for x in unit:
write_one(process(x)) # something appears immediately
Two rules from that:
Make progress visible from the first unit of work. Never batch the only observable output to the end of a long job.
Put the smallest unit first. Counter-intuitive if you're optimising throughput, but it means the watcher gets confirmation in seconds instead of a long stretch of faith. That confirmation is worth more than the scheduling efficiency, because it's what prevents the restarts that actually destroy work.
The catch: eager is not the same as unstructured
"Act immediately" can be taken too far, and I have.
Acting the moment you're ready is right. Firing off work before you understand the target is not — that's the assume-instead-of-asking failure, and it's more expensive than any amount of waiting. Eagerness applies to execution once the direction is settled, not to skipping the one clarifying question at the start.
The other guardrail: don't spawn a new waiter process every time you need to wait. I once stacked five background pollers on top of the real work, never killing the previous ones, until someone asked why so many processes were running. One reusable watcher, killed before another starts.
The verdict
Fixed idle waits are pure latency, and verbal deferrals are the same failure with better manners. Act the moment you're ready; wait only for a blocker that would actually reject the call; and when you must poll something external, derive the interval from how long that thing really takes.
Then make the work observable from the first unit, smallest first, so the human watching never has to guess whether it's alive.
Your response time equals your polling interval. Set it to zero.
Want your agent pipelines re-architected around events instead of timers? Book a call or see the work.