Rules: keep state

Last updated 2 November 2026

View .md

Machines are not open yet. These pages describe Machines as they will open; until then, a create answers region_not_offered (422), “Machines are not offered in any region yet.”

Check these rules before you rely on what your Machine keeps: each keeps its data safe across a sleep, a wake or a crash.

The examples use the database a Machine opens as Keep data in its database and Volume shows, and a node:http server.

Keep what must last in its database or on its Volume

Every wake starts your image on fresh disks with empty memory, and only the database and the Machine’s Volume keep data durably. On the Volume, a file is durable once a flush such as fsync returns; close alone is not one (see Sleep guarantees).

Do
db.prepare("INSERT INTO notes(id, body) VALUES (?, ?)").run(id, body);
await db.synced();
Don’t
writeFileSync(`/scratch/notes/${id}.json`, body); // gone at the next wake

Reconnect, and read the clock again, after a wake

A wake is a new boot: every connection your program had is gone, and so is every timer it set (see What a wake starts with).

Do
async function call(body) {
  try {
    return await upstream.send(body);
  } catch {
    upstream = await connect(); // the connection did not survive the sleep
    return await upstream.send(body);
  }
}
Don’t
const upstream = await connect(); // once, at start: fails after the next sleep
setTimeout(expireSessions, 60_000); // fires at once, or never, across a sleep

Await synced() before you answer

A reply sent before synced() resolves can announce a commit that a crash then loses (see Answer after synced()).

Do
db.prepare("INSERT INTO orders(id, item) VALUES (?, ?)").run(order.id, order.item);
await db.synced();
res.writeHead(201).end();
Don’t
db.prepare("INSERT INTO orders(id, item) VALUES (?, ?)").run(order.id, order.item);
res.writeHead(201).end(); // the order is only on this Machine's disk

Open the database with the settings the Machine gives you

The platform names the database and its cache directory, and its bound on what may wait keeps the backlog small enough to send before a sleep ends (see Open it).

Do
import { open } from "@tonbo/sqlite-ursula";

// On a Machine the platform always sets these: the database, its cache
// directory and how much may wait to be sent. It also keeps the token that
// opens the database fresh.
const db = await open(process.env.TONBO_DATABASE_URL, {
  dir: process.env.TONBO_VFS_CACHE_DIR,
  maxPendingBytes: Number(process.env.TONBO_VFS_MAX_PENDING_BYTES),
});
Don’t
import { open } from "@tonbo/sqlite-ursula";

// The default backlog can outgrow what a sleep sends in time.
const db = await open(process.env.TONBO_DATABASE_URL);

Handle SIGTERM: sync and exit within the stop grace

Every sleep sends SIGTERM and stops the Machine its stop grace later, 10 seconds by default, whatever stop signal the image names (see Stopping).

Do
process.on("SIGTERM", async () => {
  server.close(); // take no new requests
  await db.synced(); // every commit is durable from here
  db.close();
  process.exit(0);
});
Don’t
process.on("SIGTERM", () => {}); // stopped anyway when the grace ends

Finish the work before you answer, or keep the request open while it runs

Only an open request keeps a Machine awake, so work left running after the answer stops at the next sleep (see What keeps it awake).

Do
const result = await runJob(job);
db.prepare("INSERT INTO results(id, body) VALUES (?, ?)").run(job.id, JSON.stringify(result));
await db.synced();
res.writeHead(200).end(JSON.stringify(result));
Don’t
res.writeHead(202).end('{"state":"queued"}');
runJob(job); // nothing keeps the Machine awake for this