Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Durable Objects  ›  Capstone Project

Transactional Operations — All of It or None of It

Transactional Operations 15 minDurable Object storage calls hold other requests out through input gates while outside awaits let them in, and transactions make a group of changes succeed or fail together.

Transactional Operations — All of It or None of It

Hoplink gives each free user 50 links. Creating a link inside the user's Durable Object does two things: add the link and decrease the remaining allowance. If the first succeeds and the second fails, the user gets a free link. If two creations interleave, both might see "1 left" and both succeed. Transactions and the object's built-in gates stop both problems.

The rule

Storage calls hold the door; an outside await opens it.

A Durable Object runs one event at a time, but an async method can pause at an await. What happens during that pause decides whether your code is safe:

  • Input gate. While the object is waiting on its own storage (get, put, sql.exec), no other request is let in. So a read followed by a write, with only storage calls between them, cannot be interrupted.
  • Output gate. Anything the object sends out — a response, a WebSocket message — is held back until the writes before it are safely stored. Nobody hears "done" about a write that could still be lost.
  • The open door. Awaiting something outside storage, such as fetch() to another service, lets other requests run in the meantime. State you read before that await may be stale afterwards.

For changes that must succeed or fail together, wrap them in a transaction:

async createLink(code, url) {
  return this.ctx.storage.transactionSync(() => {
    const [{ remaining }] = this.ctx.storage.sql
      .exec("SELECT remaining FROM quota").toArray();
    if (remaining <= 0) throw new Error("No links left");
    this.ctx.storage.sql.exec("INSERT INTO links (code, url) VALUES (?, ?)", code, url);
    this.ctx.storage.sql.exec("UPDATE quota SET remaining = remaining - 1");
    return remaining - 1;
  });
}

If anything inside throws, every change in the block is rolled back. (transactionSync is for SQLite-backed objects; the key-value API offers ctx.storage.transaction(async (txn) => ...).)

When an object starts and must load or prepare state before handling anything, ctx.blockConcurrencyWhile(async () => { ... }) in the constructor keeps every request waiting until setup finishes.

The shortcut

Scan any method for awaits that are not storage calls. Each one splits the method into separate steps where the world can change. Do the outside work first (validate the URL, call the safety checker), then do all reads and writes in one uninterrupted stretch or one transaction.

Making createLink safeStep 1 of 4
  1. Find the outside await

    await fetch(safetyCheckerUrl) sits between reading the quota and writing it. That opens the door.

Trap: read state, await fetch(...), then write based on the old read. Another request may have changed the state in between.

Drills

Answer fast — trust the rule.

quick drill

A method reads the quota with storage.get, then immediately writes with storage.put, with no other awaits. Can another request slip in between?

quick drill

What does the output gate guarantee?

quick drill

Inside transactionSync, the INSERT succeeds and then the code throws. What is stored?

quick drill

Which line most likely creates a race inside a Durable Object method?

quick drill

An object must load a large config from storage before serving any request. What fits?

Remember

  • Storage calls hold the door; an outside await opens it.
  • Shortcut: do outside work first, then read and write in one stretch or one transaction.
  • Trap: read, await an outside call, then write from the stale read.

Practice — level up

0/1 solved

Write your answer, then compare it with a rubric and a model answer.

ExplainClose the quota race

A createLink method reads the user's remaining quota, awaits an outside safety check, then inserts the link and decreases the quota. Users occasionally exceed their limit. Explain why and fix it.