DEV Community

Teasa team
Teasa team

Posted on

Background jobs that survive a closed tab need more than a queue

A browser can stop polling at any moment: its tab closes, the phone goes offline, or the user signs out. If the browser also owns the workflow, the work stops with it.

A durable job changes that relationship. The client asks the server to accept work, receives an identity, and later observes progress. Closing the screen stops observation, not the accepted job.

The tricky failures occur between those steps: committing the job but failing to enqueue it, losing a worker halfway through a task, or letting an old worker write after a replacement has taken over.

Commit admission before acknowledging it

Create an owner-scoped job record and its initial tasks durably. Tie admission to an idempotency key so a lost acknowledgement can recover the same job.

Return the job ID and a status URL after the commit. 202 Accepted means processing has been accepted; it does not promise successful completion. The status resource should distinguish queued, running, retrying, completed, cancelled, and needs-attention states.

Every status read still requires authorization. Knowing a job ID must not grant access to another person's input or generated output. A progress list usually needs task labels and resource references, while detailed recovery data belongs behind an owner-only endpoint.

Repair the commit-to-enqueue gap

Consider this timeline:

  1. The database commits job J.
  2. The queue request fails.
  3. The API process exits.

The job exists, but no worker has been told about it. Simply retrying the queue request inside that process cannot cover every crash.

A transactional outbox can record dispatch work beside the job. Another approach is a recovery scanner that finds committed jobs with runnable tasks and dispatches them again. Either design must tolerate duplicate delivery. Queue delivery is a request to inspect durable state, not authority to execute every task again.

Leases let work move; fences stop stale writes

A lease gives a worker a bounded period to finish a task. If the lease expires, another worker can claim it. A monotonically increasing fence identifies which claim currently owns the result.

Imagine worker A holds fence 7 and stalls. Worker B claims fence 8. A wakes up with a finished result. Without a fence check, A can overwrite B's work even though the system correctly recovered the expired lease.

Here is an executable model of that decision. Save it as fence-model.test.mjs and run node --test fence-model.test.mjs:

import test from "node:test";
import assert from "node:assert/strict";

function claim(task, now, ttl) {
  const runnable = task.state === "queued" ||
    (task.state === "running" && task.leaseUntil <= now);
  if (!runnable) return null;
  task.fence += 1;
  task.state = "running";
  task.leaseUntil = now + ttl;
  return task.fence;
}

function finish(task, fence, now, value) {
  if (task.state !== "running" || task.fence !== fence ||
      task.leaseUntil <= now) return false;
  task.value = value;
  task.state = "completed";
  return true;
}

test("a replaced worker cannot overwrite the current result", () => {
  const task = { state: "queued", fence: 0, leaseUntil: 0 };
  const a = claim(task, 0, 10);
  assert.equal(claim(task, 9, 10), null);
  const b = claim(task, 10, 10);
  assert.equal(finish(task, a, 11, "stale"), false);
  assert.equal(finish(task, b, 11, "current"), true);
  assert.equal(task.value, "current");
  assert.equal(claim(task, 12, 10), null);
});

test("expiry and cancellation both reject completion", () => {
  const task = { state: "queued", fence: 0, leaseUntil: 0 };
  const fence = claim(task, 0, 10);
  assert.equal(finish(task, fence, 10, "expired"), false);
  task.state = "cancelled";
  assert.equal(finish(task, fence, 5, "cancelled"), false);
  assert.equal(task.value, undefined);
});
Enter fullscreen mode Exit fullscreen mode

These synchronous objects make the state transitions easy to test. In the service, claiming and finishing must be atomic storage operations. A separate “is my fence valid?” read followed by an unguarded write leaves another race.

When completion updates both a task and its resulting document, lock or conditionally guard the relevant records in one transaction. Cancellation and replacement must participate in that same coordination. A database fence alone cannot prevent duplicate side effects in a remote service; those need their own idempotency or reconciliation mechanism.

Checkpoint useful progress

Split the workflow at boundaries worth recovering: parsed input, completed text, saved document, generated attachment. Store successful outputs so a replacement worker can reuse them. Bound retries and move repeatedly failing work into a state the user can act on.

Separate “the document is usable” from “every optional attachment finished.” A failed thumbnail should not erase completed text. Record first usability once, then keep optional task progress separate. The interface can open the document while remaining work settles.

Make cancellation explicit. Stopping a poller is local cleanup; stopping a job is an authorized, versioned server mutation. When a user takes over editing, automatic results should attach only if the intended target is still unchanged.

Test outside the initiating session

An acceptance test should start a job, terminate the initiating client, and recover it from a new authenticated session. Then test a lost dispatch, duplicate queue delivery, lease expiry, stale completion, and cancellation during execution.

The recovery screen should show what persisted and what needs action. It should not report completion merely because a request was accepted or a worker started. That distinction is what lets users leave a tab without wondering whether they have abandoned their work.


Teasa uses durable jobs for Story and Character creation in Creator Studio. Accepted work continues after the creator closes the browser, and the account recovers its progress on return. Usable text and optional artwork have separate completion states.

Explore Teasa’s interactive AI stories to see what creators build with that workflow.

Top comments (0)