Skip to content

Keeping a Sprite Running

A Sprite pauses in two stages. It first goes warm: compute billing stops, the VM suspends, and the next inbound HTTP request wakes it in 100–500ms with process state preserved. If the Sprite stays idle long enough it transitions to cold. In-memory state is dropped and the next wake takes 1–2s.

Either way, work-in-progress stalls:

  • Open TCP connections drop on the pause, even on warm. A websocket subscriber, a queue worker’s broker connection, an agent’s streaming API call: the remote end can’t be held across the suspension.
  • Process state pauses on warm, dies on cold. A warm-paused agent loop resumes its work on wake; a cold one starts over.

The Tasks API stops that pause. Register a task; the Sprite stays up. Delete it (or let it expire); the Sprite is free to pause again.

The model: a task is a hold on the current run. While at least one task is live, the Sprite runs.

  • AI coding agents running in the background (Claude Code, Codex) that should still be there when you come back
  • Queue workers waiting on jobs from an external broker
  • Anything holding outbound connections: websockets, MQTT, replication streams

If you only need a process to come back after a pause, use a Service instead. A Service is a long-running process managed by the Sprite runtime: it auto-starts on boot, keeps running through a warm wake (the process is frozen, not terminated), and restarts automatically on a cold wake. Tasks keep the current run alive. They compose: a Service launches the agent, the agent registers a task while it’s working.

  • A web server with no other long-lived state. A Service plus the URL’s wake-on-request handles that without paying for compute while the Sprite is idle.
  • A short script. Just run it and the Sprite stays up while it runs.
  • Anything you might forget to clean up. A single forgotten task expires on its own, but a forgotten heartbeat loop keeps the Sprite billing until you notice.

All Tasks API calls go to the management socket at /.sprite/api.sock. Plain HTTP, JSON body, virtual host sprite. Inside a Sprite, sprite-env curl is a shorthand that handles the socket and host. sprite-env curl -X POST /v1/tasks -d '...' is equivalent to the long form below; this guide uses the explicit curl --unix-socket form to keep the wire-level mechanics visible.

Create a task. The Sprite is now held in an active state for an hour (the maximum lifetime per task):

Terminal window
curl --unix-socket /.sprite/api.sock \
-H "Content-Type: application/json" \
-X POST http://sprite/v1/tasks \
-d '{ "name": "agent", "expire": "1h" }'

Refresh it before it expires:

Terminal window
curl --unix-socket /.sprite/api.sock \
-H "Content-Type: application/json" \
-X PUT http://sprite/v1/tasks/agent \
-d '{ "expire": "1h" }'

Release the hold when done:

Terminal window
curl --unix-socket /.sprite/api.sock \
-X DELETE http://sprite/v1/tasks/agent

A single task expires after at most an hour, so anything that needs to run longer uses a heartbeat: a short expiry, refreshed on a shorter interval, deleted on exit. If the process crashes without cleaning up, the task expires on its own and the Sprite pauses.

#!/usr/bin/env bash
set -euo pipefail
api() {
curl -s --unix-socket /.sprite/api.sock \
-H "Content-Type: application/json" "$@"
}
trap 'api -X DELETE http://sprite/v1/tasks/agent' EXIT
while true; do
api -X PUT http://sprite/v1/tasks/agent -d '{ "expire": "5m" }' >/dev/null
sleep 60
done

A 5-minute expiry refreshed every minute is a reasonable default: four missed heartbeats of margin before the Sprite frees itself, and short enough that a forgotten task doesn’t drag on.

Terminal window
curl --unix-socket /.sprite/api.sock http://sprite/v1/tasks

You should see your task with a future expires_at. If the list is empty, the Sprite isn’t held and will pause on the next idle window.

HTTP/JSON over /.sprite/api.sock, virtual host sprite.

GET /v1/tasks
{ "tasks": [ { "name": "agent", "started_at": "...", "expires_at": "..." } ] }
GET /v1/tasks/:name

200 with the task, 404 if missing.

POST /v1/tasks
{ "name": "my-task", "expire": "1h" }

expire is seconds (integer) or a duration string ("30m", "1h"). Maximum is 1 hour; longer holds require refreshing (see Upsert below). 201 on success, 409 if the name is taken.

Either form works. Creates the task or refreshes its expiry. Returns 200.

PUT /v1/tasks/:name
{ "expire": "1h" }
PUT /v1/tasks
{ "name": "my-task", "expire": "1h" }
DELETE /v1/tasks/:name

204 on success, 404 if missing.