every claude code user gets their own linux box: 8 seconds to boot, $0 when idle

by Mehdi Gatbache · numbers measured in production, july 2026 · this is the infrastructure behind Yado

I build Yado, an iOS app that runs Claude Code in the cloud so nothing of yours has to be on. No laptop, no VPS, no server. Sounds nice until you do the math: every user needs an actual computer. Real Ubuntu, their repos, a shell, Claude Code installed.

I'm one guy competing with funded startups. So the constraints were brutal and simple: signup must feel instant, an idle user must cost ~nothing, and one person has to run all of it.

Here's what came out. Real numbers, one war story, jank included.

a dumb box and a bossy backend

One rule decides everything: the container knows nothing. No business logic. No credentials in the image. Every box is byte-identical. All the brains live in a Rails backend that configures boxes from the outside by sending shell commands.

So the box only needs a tiny generic API. Mine is a Bun server: ~700 lines of TypeScript, zero npm dependencies. Five endpoints:

Cloning repos, injecting credentials, wiring git — all of it is just the backend hitting /exec. Sign out and the backend wipes the secrets the same way. The image never changes because a user changed.

The auth is one small file:

import { createHmac, timingSafeEqual } from "crypto";

export function validateSignature(url: URL): boolean {
  const userId = url.searchParams.get("user_id");
  const expires = url.searchParams.get("expires");
  const sig = url.searchParams.get("sig");

  if (!userId || !expires || !sig) return false;
  if (Math.floor(Date.now() / 1000) > parseInt(expires, 10)) return false;

  const expected = createHmac("sha256", SIGNING_SECRET)
    .update(`${userId}:${expires}`)
    .digest("hex");

  if (sig.length !== expected.length) return false;
  return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Fine print, because this is where a careful reader pokes: the signature covers who and until when. Not the command. Not even the machine — the secret is fleet-wide and routing is a separate header. What actually holds the tenant boundary is custody: these URLs are minted and spent between my backend and relay, server-side. The phone never holds one. Spelled out: a leaked URL would be up to an hour of arbitrary shell on that box, with no revocation — which is why the URLs never leave my servers, and why per-machine secrets is the upgrade this design owes itself. It's on the list.

The whole container contract:

POST /exec?user_id=42&expires=1753…&sig=…
{
  "command": "git clone https://…",
  "timeout": 60,
  "cwd": "/home/devuser/dev"
}
→ {
  "exit_code": 0,
  "stdout": "…",
  "stderr": "",
  "duration_ms": 1840
}

None of this is novel. That's the point. It survives production because there's almost nothing in it. Zero dependencies isn't a security flex either — the marquee endpoint literally runs shell commands. The win is there's nothing to patch on a random Tuesday.

the suspend trick

Each user gets one Fly.io machine — a small Firecracker microVM running Ubuntu, with a deliberately lean image: anything Claude can apt-get install itself stays out of it.

Fly machines have two idle states and they are not interchangeable. Stop resets the disk. Suspend keeps everything — memory snapshot, disk, all of it — and resumes in ~0.7s. I use suspend for everything. Stop only exists for image updates.

A suspended machine bills zero CPU and zero RAM. You pay rootfs storage: $0.15/GB/month. So my fleet's default state is asleep. A box runs while its user is in a session; they background the app, Claude finishes its turn, the backend suspends the box. The "$0 when idle" in the title is compute. Full honest number: $0 plus pennies of disk.

pathmeasured
resume a suspended box (returning user)~0.7 s
reopen when the box is already running0.3 s median
signup → working box (warm pool hit)~8–10 s
cold create, image pull included (pool miss)~18–26 s

the warm pool

That last row is the enemy. 18–26 seconds of image pull on a fresh host. A signup staring at a spinner for half a minute leaves.

First attempt: Fly's skip_launch — pre-create machines without booting them. Useless. Measured it: the image isn't pulled until first boot, so a "pre-created" box still eats the whole pull on the user's critical path.

You have to warm the machine all the way through:

Step 3 matters more than it looks: running Claude once before the snapshot puts the binary in the page cache the snapshot preserves. Claimed boxes wake up warm.

Signup claims a box, resumes it, and personalizes it over /exec while the user is still finishing OAuth. ~8 seconds, tap to live box. Last 45 days: every signup came from the pool, zero cold paths. Yes — that stat also tells you my volume is modest. The pool is sized for the spike I'm hoping for, not the traffic I have.

The unglamorous parts are the real work — Fly machines keep the image they were created with, so every release drains and rebuilds the whole pool, and a small zoo of janitor jobs keeps the rest honest. None of it is clever, but all of it got debugged in production.

the autoupdater that bricked everything

Best incident. Claude Code's installer makes ~/.local/bin/claude a symlink and its background autoupdater repoints that symlink at runtime. No version compare. Fine on your laptop with one Claude. My boxes run several — chat session, terminal session — which means several autoupdaters racing to swap the same symlink. Race loser leaves it dangling or downgraded. Every session on the box dies. (anthropics/claude-code#14100.)

Fix: DISABLE_AUTOUPDATER=1 fleet-wide. I own freshness now — a controlled claude update at pool warm-up and at idle-suspend, when zero sessions run, plus a health check at claim time as the net. Boring. Works.

recovery by suicide

The entrypoint is ~100 lines of bash. My favorite ten: a watchdog that probes the box's own /health, and after two minutes of silence kills PID 1. Fly's restart: on-failure does the rest — full cold restart.

It targets one specific failure: userspace wedges (memory pressure, deadlock) while the VM still looks alive to Fly. The probe is shallow on purpose — "is the API process alive" is exactly what that mode kills. It has quietly saved boxes I never had to think about.

Everything else is idempotence: setup can re-run at any session start, so the answer to half the weird states is "run setup again."

the economics

Users bring their own Claude subscription. Claude Code in the box authenticates against their Pro/Max account, Anthropic bills them, my marginal AI cost is zero. No GPU line item. I pay compute during live sessions and pennies of storage the rest of the time. This is the only shape of this product one person can afford to run — and the honest answer to "how is the free tier sustainable?"

The catch: that model lives on Anthropic's terms — pricing and policy on programmatic subscription use. They announced a change in 2026 that would have metered this kind of usage separately, then postponed it indefinitely. They could reprice it, or decide one day it's not allowed at all — that's the actual size of the risk, and I build knowing it. What I sell is the phone-first workflow on top; the layer underneath is theirs.

the janky parts I'm keeping

what I'd change starting today

Since I built this, Fly productized almost exactly this layer: Sprites. Persistent Linux machines for agent workloads — suspend/wake, exec/filesystem API, checkpoints, and a per-sprite authenticated URL. That last one deletes my header dance. Checkpoints would give me "restore the box to before Claude broke it" for free. Starting from zero, I'd evaluate it hard before writing any of the above.

I'm not migrating. This system is working, boring, and fully understood — a rewrite resets all three to zero, on live user infra, for no user-visible win, and I'm the only one on call.

If you're building per-user compute for agents: steal the pattern. Dumb box, bossy backend, HMAC on every request, suspend as the default state, warm pool for the first impression. ~700 lines of container, one Rails app, real users on the App Store since June.

live your life, keep shipping.

Download on the App Store

free · bring your own Claude account