shedix

Auto-sleep for agent machines: telling busy from idle

An agent's machine spends much of its life waiting: for you to read an answer, for the next task, or for nobody at all once the work is done. shedix prices machines by the running hour, so the rule that decides when a machine is idle decides what a forgotten machine costs. This post explains our rule, why each part is there, and where it still falls short.

Why idle detection sets the bill

An M costs $0.16 an hour while it runs. Left on over a 48-hour weekend, that's $7.68. With auto-sleep, a forgotten M costs about $0.16, the idle hour before it stops.

Sleep is a full stop. The hourly price stops, and the disk and every file on it stay; Google charges for a disk whatever state its VM is in (instance lifecycle). Running processes end, and starting the machine again takes under a minute: 47 seconds in one test.

So the two mistakes cost different things. Sleeping too early ends someone's running work and costs them a restart. Sleeping too late costs an hour's price at a time. We wait a full hour, and we count generously.

What we count as use

Every 30 seconds, a small script on each machine decides whether the machine was in use. It counts:

  • a signed-in SSH session running a shell or a command;
  • an open terminal tab in the browser;
  • an open VS Code tab;
  • Claude Code or Codex writing to a conversation file in the last two minutes;
  • the CPU busier than a quarter of a core, averaged over the 30 seconds;
  • more than 1 MB of network traffic, in and out, in 30 seconds.

The first four are a few lines of shell:

in_use() {
  pgrep -u "$USER" -f '^sshd(-session)?: [^ ]+@' >/dev/null ||
    [ -n "$(tmux list-clients 2>/dev/null)" ] ||
    [ -n "$(ss -Htn state established '( sport = :7683 )' 2>/dev/null)" ] ||
    [ -n "$(find ~/.claude/projects ~/.codex/sessions -type f -mmin -2 -print -quit 2>/dev/null)" ]
}

Line by line:

  • SSH. Once someone has logged in and opened a shell or run a command, OpenSSH titles that session's process with the user and terminal, like sshd: sam@pts/0. A connection that never logs in never gets that title.
  • Terminal tabs. Each browser tab on our web terminal is a tmux client, so this sees open tabs, not tmux sessions left running with nobody attached.
  • VS Code. A live connection to the port VS Code in the browser listens on.
  • Agents. Recent writes where Claude Code keeps its transcripts and Codex its threads.

The CPU and network figures come from the kernel's counters in /proc/stat and /proc/net/dev, sampled 30 seconds apart. The script reads process names, connections, counters and file times, never file contents.

When it sees use, it records the time and reports it to Google's metadata server as a guest attribute, at most once a minute. That suits what Google's docs say guest attributes are for: small values that don't change often.

Our control plane reads those reports every 5 minutes. For each running machine that isn't kept awake and has been up for over an hour, it ignores any report from before the machine's last start, and stops the machine if its last use is more than 60 minutes old. The stop is logged as a sleep by the system. Machines on images too old to report never sleep.

We watched it work on 1 November: a test machine last used at 01:44:38 UTC was stopped by the 02:45 run, at 02:45:14, 36 seconds after its hour was up.

What we don't count, and why

  • Connections to port 22. SSH is open to the internet, with keys only, and scanners knock all day. Counting connections would keep every machine awake, so only a logged-in session counts.
  • Phone access on its own. With phone access on, Claude Code's Remote Control keeps a process running and connected. Counting it would mean such a machine never sleeps. When the agent is actually working it writes its conversation, and that counts.
  • tmux sessions nobody is attached to. A job left running in one counts through what it does (CPU, network, an agent's writes), not through the session.

One choice cuts the other way. An open session counts even when nobody is typing, so a forgotten browser tab or SSH session keeps a machine awake for as long as it stays open. We preferred that to cutting someone off in the middle of a session. Nothing caps it yet.

How quiet an idle machine is

Thresholds only work if idle sits well below them. On a test machine on 1 November, idle measured 0.01 of a core and about 17 KB of network per 30 seconds. The thresholds are 25 times and about 60 times higher, so background noise doesn't keep a machine awake, while an install still counts.

Daniel P Gross did the same when he made his home server sleep on idle: he noted his two measures at idle and again when the server was busy, then picked the rule.

Long jobs nobody is watching

The weak spot is work that is slow but quiet. A build or test suite that keeps the CPU busy counts. A script that sleeps for an hour before deploying doesn't, nor does a crawler that waits between requests, nor Claude Code waiting for your answer, since it writes nothing while it waits. After an hour, the machine stops and the process ends. For a conversation, claude --continue picks it up after a restart. For everything else, there's Keep awake, a switch per machine:

curl -sS -X PATCH "$SHEDIX_URL/api/machines/$NAME" -H "Authorization: Bearer $SHEDIX_TOKEN" \
  -H "Content-Type: application/json" -d '{"keepAwake": true}'

The machine page shows when a machine will go to sleep, and our guide tells agents to turn Keep awake off when the work is done.

Other answers

A cron job and a suspend. Daniel P Gross's script runs every 10 minutes, checks who for logged-in users and lsof for Time Machine connections, and calls systemctl suspend when both are quiet. Waking takes wake-on-LAN for ordinary packets, plus a Raspberry Pi answering ARP for the sleeping server. A Compute Engine VM can't put itself to sleep that way: Google ignores systemctl suspend from the guest. Our machines also hold no cloud credentials, so they only report, and the decision is made outside, through the API.

Pause within seconds. Fly.io's Sprites count a running command, a session producing output, an open connection to the Sprite's URL or a service handling traffic. After about 30 seconds without any of them, a Sprite pauses: first warm, suspended with its memory and back in 100–500 ms with processes intact, later cold, waking in 1–2 seconds with processes started fresh. For background agents, a Tasks API holds a Sprite awake for up to an hour per task, renewed with a heartbeat.

Count the person, not the work. GitHub Codespaces stop after 30 minutes by default, configurable from 5 to 240 minutes. Inactivity there means "the absence of activity indicative of a user's presence": no typing, no mouse, no terminal input or output.

The idle window follows the cost of waking. Sprites can pause after 30 seconds because a warm wake is quick and keeps processes. Ours is a fresh boot of about a minute that ends every process, so a long window is the safer default. Compute Engine's suspend keeps memory, though Google keeps charging for it while suspended. It's the change that could let us shorten the hour, and we haven't built or measured it yet.

What sleeping saves at each size is in our post on what an always-on agent machine costs.