Browser Worker AI Task Protocol: Cancellation, Backpressure, and Generation Isolation
Moving inference into a Worker only prevents main-thread jank; it doesn't automatically solve out-of-order results, backlog, or cancellation. This article designs a decodable message envelope, cooperative stopping, hard termination, and generation isolation protocol.
Key Points
- Topic Focus: AI、TypeScript、DevOps
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Moving embedding, image preprocessing, or on-device inference into a Web Worker keeps the UI responsive, but doesn't guarantee reliable concurrency. Old tasks can overwrite new results, large ArrayBuffer clones can double memory, and synchronous Wasm can also block cancel. Web Workers Concurrency Basics covers threads and postMessage; this article handles task protocols and failure boundaries.
Messages must be independently verifiable and ordered
Both sides use a versioned envelope. The entry point first decodes event.data as unknown, then checks types, lengths, and enums before entering the state machine; a TypeScript interface cannot prevent malformed runtime data.
type WorkerCommand = {
protocol: 3;
kind: "start" | "cancel" | "dispose";
taskId: string;
generation: number;
sequence: number;
payload?: unknown;
};
type WorkerEvent = {
protocol: 3;
kind: "accepted" | "progress" | "result" | "canceled" | "failed";
taskId: string;
generation: number;
sequence: number;
payload?: unknown;
};
taskId identifies one logical unit of work, generation increments on each rerun, and sequence increases monotonically within that generation. The main thread only applies events for the current generation with a larger sequence number; even if an old Worker emits a result just before termination, it cannot rewrite the current view. When the protocol version is incompatible, return a bounded error and rebuild the Worker; never guess field meanings.
The release checklist records the Worker bundle hash, model/tokenizer digest, protocol and decoder library versions, and browser matrix digest. Reports store full Chromium, Firefox, and WebKit build numbers and the operating system; feature support is detected at runtime, not just by User-Agent.
Cancellation is split into cooperative and hard termination
cancel suits tasks that can frequently yield the event loop. The Worker checks the canceled flag at chunk boundaries, releases tensors, Wasm memory, and temporary caches after stopping, then sends canceled. Do not assume that a main-thread AbortSignal can be passed through directly in all browsers; the protocol only carries a stable ID and a reason.
A long synchronous loop or uninterruptible Wasm will not process the queued cancel. Once the cancellation deadline is exceeded, the main thread calls worker.terminate(), discards every generation of that Worker, and creates a new instance. Hard termination can only be used for tasks with no external side effects or with idempotent recovery; CPU/GPU already consumed for in-browser computation still has to be metered. The cancellation state can connect to the Agent Runtime State Machine, but Worker termination is a process-resource boundary.
AbortController handles main-thread fetch, timeouts, and component unmounting, and maps the reason to a protocol message. For end-to-end cleanup, see Next.js AI Stream Response Governance; you can't just stop the React updates.
When the queue is full, stop enqueuing and keep draining
postMessage() has no desiredSize or awaitable backpressure; after serialization, a command first enters the browser's port message task queue. The Worker can only count the number of waiting tasks or return queue_full after receiving the message, and cannot limit messages and Transferables already queued before that. Therefore the application queue cap cannot masquerade as the hard limit of the browser's internal queue.
At Worker startup, task credit and byte credit are issued. Before calling postMessage(), the main-thread scheduler atomically reserves credit based on the number of tasks and the payload/Transferable bytes; when credit is insufficient, it only enters another bounded upstream queue or immediately returns queue_full. accepted only moves the reservation from "sent" to "Worker waiting/running" and does not refund credit; only result, canceled, failed, or explicit rejection refunds it. Cancellation, send exceptions, and Worker crashes must also reclaim local credit by Worker generation while keeping the execution result as unknown; releasing the resource ledger must not be mistakenly recorded as "the business task never ran."
Inside the Worker, the number of running tasks, waiting tasks, and the age of the oldest task are still bounded; once the high watermark is reached, accepted tasks keep being processed. Interactive tasks can supersede older generations in the same session; pausing Worker consumption only widens the backlog. All start calls must go through the same send scheduler; calling postMessage() directly while bypassing credit is a protocol violation.
Large binary blocks should transfer ownership through a transfer list; after sending, the main thread must treat the buffer as detached. Reserve byteLength from byte credit before sending; if postMessage() throws, refund it immediately, and release the credit for successfully sent data only after the terminal state. The receiver re-checks shape and length; Transferable avoids copying but does not automatically guarantee the content is trustworthy. When SharedArrayBuffer is needed, satisfy cross-origin isolation separately and use Atomics to define producer/consumer cursors; do not treat shared memory as a normal array.
Worker crashes, messageerror, or timeouts move all in-flight tasks to unknown; the upper layer decides on retries based on idempotency. Do not assert that a task never executed just because no result was received. AI drafts can save inputs and checkpoints; GPU/Wasm intermediate objects cannot serve as durable facts.
Prove the protocol with race tests
Automated cases cover duplicate start, out-of-order progress, late old generation arrivals, cancel after result, misreading a transferred buffer, credit exhaustion, send exceptions, Worker throws, a synchronous infinite loop, and page unload. Submit a burst of start calls far exceeding credit within the same microtask, and assert that the outstanding tasks/bytes actually passed to postMessage() never exceed credit; deliberately let old tasks finish after new ones, and confirm the UI only adopts the new generation. After hard termination, verify that the Worker count, memory, credit, and event listeners fall back.
The release gate can require: zero cross-generation pollution; cancel P95 within budget; no old event takes effect after hard termination; the application send side's outstanding never exceeds credit; all three approved browser matrices pass. The browser's internal port queue is not directly observable, so the gate cannot describe it as a measured hard limit. The new protocol first supports dual decoding and only sends the old format, then switches a small number of users to v3; v2 writes are stopped only after it is stable. Rollback restores the old bundle and the protocol's send version, while new pages still keep the v3 decoder until the observation period ends, so that messages from old tabs don't crash the rolled-back page.
The essence of Worker reliability is not "the background thread is running", but that every command can be verified, every event can be attributed to a generation, every cancellation has a deadline, and every overload can be rejected. Only then will the main thread not use a smooth UI to mask a background that has already fallen out of order.
Further reading
Related
Related Content
Article
What Is Vibe Coding? Will It Change How Programmers Work?
Technical trends, engineering practices, and an indie developer perspective on Vibe Coding, AI, and independent development.
Article
From Copilot to Codex: AI Coding Tools Enter the Agent Era
Technical trends, engineering practices, and indie-developer perspectives around AI, AI Agents, and Vibe Coding.
Article
Complete Technology Stack Analysis for LLM Application Development
Covers tech trends, engineering practices, and indie developer perspectives around large models, RAG, and AI.
Article
Is Next.js App Router a Good Fit for a Technical Community?
Technical trends, engineering practices, and independent development perspectives centered on Next.js and React.