DEV Community

Cover image for Stop Paying Your Agent to Wait
Andre Burgstahler
Andre Burgstahler

Posted on Originally published at Medium

Stop Paying Your Agent to Wait

Long jobs, polling and background work without burning tokens. Every check-in re-sends the whole conversation, so make waiting one quiet command, and for really long jobs write down the state and walk away.

Abstract illustration: on a dark navy background, an off-white hourglass stands on a thin teal line, while to its left a row of amber clock faces trails away, each smaller and fainter than the last
One wait, not a hundred check-ins.

TL;DR

  • When an agent "keeps an eye on" a long job, each check is a new model request that carries the whole conversation so far. Caching makes those requests cheaper, not free. A session that checks every few seconds for half an hour can send millions of input tokens and produce nothing.
  • Make waiting a single tool call. One quiet command that blocks until the job ends and then prints a short summary costs about one turn. I've included a tested 61-line wait-for.sh for containers, processes, URLs, files and "run this until it passes".
  • For jobs that take hours, don't keep the session open at all. Write a status file, end the session, and start a fresh one when the job is done. Per-tool settings for Codex, Claude Code and OpenCode are below.

This week someone in r/codex posted a bug report with a title that says it all: Codex exhausts the entire usage quota busy-waiting. They had started a long task, and the agent spent the time checking on it, again and again, until the quota was gone.

Their band-aid was to tell the agent to sleep ten minutes, check once, then sleep again. That helps. One reply in the thread put it better: wrap the job "in a script that blocks until the container exits, then prints the last few hundred lines of docker logs. one tool call, one result, zero polling turns."

That's the whole idea of this article. The rest is why it works, a script that does it, and what each tool gives you to stop the polling.

Why waiting costs anything at all

A coding agent doesn't watch anything. It takes turns. Each turn is a request to the model that includes the system prompt, your instructions, the tool definitions and the conversation so far, plus whatever the last tool call returned. Then the model decides what to do next.

So when the agent "checks on the build", that check is a full request. If the build isn't done, the model reads "still running", decides to check again, and makes another full request. The job's progress doesn't change what each check costs. The size of the conversation does.

Prompt caching softens this. The repeated part of each request is billed as cache reads, which cost much less than fresh input. But much less isn't zero, and on a subscription plan cache reads still count toward your limits. I went through how those limits fill up in Where Did My Claude Usage Go?

Two bars compare one 30-minute wait with a 100,000-token conversation. Checking every 5 seconds: 360 requests, about 36 million input tokens re-sent. Checking every 5 minutes: 6 requests, about 600,000 tokens. One blocking command: 1 request to start and 1 with the result, about 200,000 tokens.
Illustrative arithmetic, not a measurement. The pattern holds at any context size: cost grows with the number of check-ins, not with the length of the job.

Two numbers drive the cost: how big the conversation is, and how many times the agent looks. You can't do much about the first while a task is in flight. You can do a lot about the second.

The job's progress doesn't change what a check costs. The size of the conversation does.

Three ways to wait

There are three ways to wait, and each one is cheaper than the one before it.

A three-step ladder. Step 1, check in from the chat: the agent runs a status command, reads the result, and repeats; cost grows with every check. Step 2, one blocking command: a quiet script waits for the job and returns one short summary; cost is about one turn. Step 3, write it down and leave: the agent writes a status file and ends the session, and a fresh session reads the file when the job is done; cost is zero while you wait.
Most agents start at step 1. Moving to step 2 is a one-line rule in AGENTS.md.

1. Check in from the chat. The agent runs docker ps or gh run view, sees "in progress", and runs it again. This is the default when nothing tells the agent otherwise, and it's what drained the quota in that thread.

2. One blocking command. The agent runs a single command that waits for the job, stays silent while waiting, and prints one short result: done or failed, how long it took, and the last lines of the log. The model sees one tool call and one answer.

3. Write it down and leave. For jobs that run for an hour or more, even one open session is wasteful. The agent writes what it started and how to check it into a file, then ends its turn. When the job is done, you (or a scheduler) start a fresh session that reads the file. The wait costs nothing because there's no session.

The wait script

Some commands already block. docker wait blocks until a container stops and prints its exit code. gh run watch <id> --exit-status blocks until a GitHub Actions run finishes and exits non-zero if it failed. But it reprints progress every few seconds, so send its output to /dev/null or the agent reads all of it.

For everything else, here's wait-for.sh. It waits for one of five things, prints nothing while it waits, and then prints a single summary line followed by the last lines of the relevant log:

Exit codes follow the usual conventions: 0 when the job finished well, 1 when it failed, 124 on timeout (the same code coreutils timeout uses), 2 for a usage error. Add --status FILE and it also appends a timestamped line when it starts and when it ends, which matters for the third way to wait.

Terminal output from wait-for.sh. The command waits 3 seconds for a container and prints "wait-for: DONE (exit 0) after 0m03s: container wf-ok" followed by the last three log lines. A failing container prints "FAILED (exit 3)" and exits 1. A container that runs past the time limit prints "TIMEOUT after 0m03s" and exits 124.
What the model sees: one line of verdict, then just enough log to act on.

Here's the full script. I tested it with bash 5 and with bash 3.2, the version macOS ships, and it doesn't depend on GNU timeout:

I tested it on 21 October against real containers (Docker 26.1.5), a real process, a local HTTP server that starts late, a file that appears late, and the timeout and usage-error paths. All 13 cases passed, and shellcheck reports nothing.

Two things to know before you rely on it. First, it can only wait as long as your agent's tool call is allowed to run. The next section covers those limits. Second, --pid only works for a process on the same machine and in the same sandbox as the script. If your agent's shell runs in a sandbox, start the job from that same shell.

Tool by tool

Each tool handles long commands differently, and the defaults decide how often the model gets a chance to check in.

Codex Claude Code OpenCode
A command that outlives one call Returns a session ID after up to 30 s; the model polls it with write_stdin Moves to the background when it hits its timeout (2 min default, 10 min max) Stopped at its timeout (2 min default) unless the model raises timeout
Background mode A still-running command stays open as a background terminal; /ps lists them run_in_background; /tasks lists and stops them background: true, no timeout by default
How the model hears it finished By polling with write_stdin; each empty poll waits 5 s to 5 min Notification when the task ends Notification; the tool tells the model not to poll
The setting that matters background_terminal_max_timeout BASH_MAX_TIMEOUT_MS timeout per call

Codex

Codex runs shell commands through exec_command. If a command is still running after its yield time (10 seconds by default, 30 at most), the call returns with a session ID, and the model checks back with write_stdin and empty input. That empty write is a poll.

In the current source, a poll waits between 5 seconds and background_terminal_max_timeout, which defaults to 300,000 ms (5 minutes). The tool's own description tells the model "empty polls wait 5000-300000 ms by default." The wait ends early if the process exits, but not when new output arrives. So the cost of a long job depends on what wait the model asks for. A model that asks for nothing gets 5-second polls.

Three things help:

  • Raise the ceiling. In ~/.codex/config.toml, set background_terminal_max_timeout = 1800000 for a 30-minute maximum poll window. The source doesn't enforce an upper limit beyond the 5-second floor.
  • Ask for long polls in AGENTS.md. "When polling a background terminal with no input, use the maximum wait." The model reads the parameter description, but a plain rule removes the doubt.
  • Keep the job quiet. Each poll returns whatever output arrived since the last one, up to the output budget (10,000 tokens by default). A chatty build makes every poll expensive. wait-for.sh prints nothing until the end.

/ps shows your background terminals with their most recent output, and /stop stops them all. That's useful when you come back and find a poll loop you didn't ask for.

Claude Code

Claude Code's Bash tool has a two-minute default timeout per command and a ten-minute ceiling (BASH_DEFAULT_TIMEOUT_MS and BASH_MAX_TIMEOUT_MS raise both). A foreground command that hits its timeout isn't killed. It moves to the background, unless the command starts with sleep.

For anything longer, Claude can run the command with run_in_background. The output goes to a file, and Claude is notified when the task ends. You can list and stop background tasks with /tasks. In local interactive sessions, background commands have no time limit. In unattended runs (-p, the SDK, CI) they're stopped after 30 minutes by default, or after their own timeout (at most 2 hours). In a -p run started with a text prompt, the default is 10 minutes.

Claude Code also had the same habit this article is about. The v2.1.300 release notes include "Fixed Claude polling a background command's output file or sleeping while it runs." If you're on an older version, update first.

There's also a Monitor tool, which streams each output line of a background command to Claude as an event, with a 5-minute deadline by default and 30 at most. It's built for watching logs, not for waiting quietly, and it isn't available on Bedrock, Vertex AI or Foundry. For a quiet wait, a background wait-for.sh is a better fit: one notification, one short result.

OpenCode

OpenCode's V2 shell tool has a two-minute foreground timeout that the model can change per call with timeout. With background: true, the call returns immediately, has no timeout unless one is set, and the session is notified when the command finishes. The tool description is blunt about it. It tells the model "DO NOT poll for completion" and, if there's nothing else to do, to "end your response; you will be resumed automatically."

So OpenCode's design already avoids polling, as long as the model actually uses background. If yours keeps calling sleep and checking, add the rule below to AGENTS.md and name the tool option explicitly.

For jobs measured in hours: write it down and leave

A blocking command still keeps one session open. If the job takes three hours, that session sits there, and if anything wakes it up, the whole context gets re-sent again. For long jobs, end the session.

The pattern is simple:

  1. The agent starts the job in the background, detached from the session, with output going to a log file.
  2. It writes a status file: what it started, the command, the log path, how to check whether it's done, and what to do next in either case.
  3. It ends its turn. You close the session.
  4. When the job is done (you get a CI email, or a scheduler fires), start a fresh session with a prompt like "Read STATUS.md and continue."

A loop in four steps. Start the job in the background with output to a log. Write STATUS.md with the command, log path, how to check it and what to do next. End the session; nothing runs on the model while the job works. Start a fresh session that reads STATUS.md, checks the result and continues. A side note shows the command for each tool: claude -p, codex exec, opencode run.
The status file carries the state. The new session starts small.

The fresh session starts with a small context: your instructions, AGENTS.md and the status file. That's the same argument I made in Compact, Clear or Hand Off?: keep the state in files and treat sessions as disposable. One reply in the /goal thread put it well: "Compaction cycles will degrade everything inside the task's context, so the plan and success criteria need to exist outside of that."

For the "fresh session" part, each tool has a non-interactive mode you can call from a script or a CI step:

claude -p "Read STATUS.md. The job it describes has finished. Check the result and continue."
codex exec "Read STATUS.md. The job it describes has finished. Check the result and continue."
opencode run "Read STATUS.md. The job it describes has finished. Check the result and continue."
Enter fullscreen mode Exit fullscreen mode

Codex can also continue the previous non-interactive run with codex exec resume --last "...". That keeps the old conversation, which is sometimes what you want and sometimes exactly what you were trying to leave behind.

A good status file is short. This is the shape I use:

# STATUS (updated by the agent; read this first)
Job:      nightly e2e suite, container e2e-0421
Started:  2026-10-21T18:05+02:00
Check:    .agents/bin/wait-for.sh --timeout 1m --container e2e-0421
Log:      docker logs --tail 200 e2e-0421
If it passed:  open the PR from branch e2e-fixes, link this run.
If it failed:  fix only the first failing test, re-run just that file.
Stop rule:     if the same test fails 3 runs in a row, stop and ask.
Enter fullscreen mode Exit fullscreen mode

The last line borrows from the same thread, where one person stops a loop "if progress <1% per loop 3 times in a row." Whatever your threshold, write it down. A loop with no stop rule will run until something else stops it, usually your quota.

Put it in AGENTS.md

AGENTS.md is the one instructions file Codex, Cursor and OpenCode all read, and Claude Code reads it too when there's no CLAUDE.md. (If you keep a CLAUDE.md, put @AGENTS.md at its top.) Keep the rule short. I argued in How Much Should You Invest in Your AGENTS.md? that every line there costs context on every turn.

## Waiting for long jobs
- Never poll in a loop. To wait for a job, run ONE command that blocks until it ends:
  `.agents/bin/wait-for.sh --timeout 45m --container NAME` (or --pid, --url, --file, -- CMD).
- Commands that print progress (gh run watch, npm ci, docker build): send progress to a log file,
  then read only the tail.
- Codex: when polling a background terminal with no input, use the maximum wait.
- OpenCode / Claude Code: start long jobs in background mode and end your turn. Don't sleep and check.
- Jobs over ~30 minutes: start detached, write STATUS.md (job, check command, log, next steps,
  stop rule), then stop and tell me.
Enter fullscreen mode Exit fullscreen mode

Checklist

  1. Find the waits. Look for long jobs in your workflow: test suites, container builds, CI runs, migrations, model downloads.
  2. Give each one a blocking command. docker wait, gh run watch --exit-status > /dev/null, or wait-for.sh.
  3. Make it quiet. Progress goes to a log; the agent gets a verdict and a tail.
  4. Check your tool's limits. Codex: raise background_terminal_max_timeout. Claude Code: update to v2.1.300 or later, and know your background limits in -p and CI. OpenCode: make sure long jobs use background.
  5. Write the rule into AGENTS.md (above).
  6. For jobs over half an hour, end the session. Status file in, fresh session out.
  7. Add a stop rule. Three identical results in a row means stop and ask.

Threads that prompted this

Sources


If this was useful, subscribe to Agents in Practice for one practical note on AI coding agents a week. Helping your team adopt Claude Code or Codex safely and without the surprise bill? I consult on exactly that: andre-burgstahler-9n5.pages.dev.

This article was written with AI assistance and reviewed by the author.

Top comments (0)