<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Cubert Wang</title>
    <description>The latest articles on DEV Community by Cubert Wang (@cuberwang).</description>
    <link>https://dev.to/cuberwang</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4026952%2Fe5a37946-8b7d-4dd5-bbd1-0e59e89355cb.PNG</url>
      <title>DEV Community: Cubert Wang</title>
      <link>https://dev.to/cuberwang</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cuberwang"/>
    <language>en</language>
    <item>
      <title>Where async work goes to die, and how I made it observable</title>
      <dc:creator>Cubert Wang</dc:creator>
      <pubDate>Wed, 22 Jul 2026 12:24:10 +0000</pubDate>
      <link>https://dev.to/cuberwang/where-async-work-goes-to-die-and-how-i-made-it-observable-2873</link>
      <guid>https://dev.to/cuberwang/where-async-work-goes-to-die-and-how-i-made-it-observable-2873</guid>
      <description>&lt;p&gt;There's a particular kind of bug that only shows up on systems you didn't know you were running. A user clicks a button. A job is queued. The worker picks it up. The worker crashes or just hangs. The user refreshes. The dashboard says &lt;em&gt;processing&lt;/em&gt;. The founder finds out a couple of days later in a support email.&lt;/p&gt;

&lt;p&gt;The problem is not that queue-based work is fragile. It is that async work crosses invisible boundaries, and most stacks give you no way to inspect what happened at each one. The job is &lt;em&gt;in flight&lt;/em&gt;. The job is &lt;em&gt;done&lt;/em&gt;. The job is &lt;em&gt;lost&lt;/em&gt;. Those three states collapse into the same status field, and the rest is hope.&lt;/p&gt;

&lt;p&gt;I got tired of the hope. This post walks through how I rewired my async pipeline so each boundary has its own inspectable state. The architecture sits behind a small multimodal learning product I run solo.&lt;/p&gt;

&lt;h3&gt;
  
  
  The shape of the problem
&lt;/h3&gt;

&lt;p&gt;Async work in a serverless stack has at least five boundaries, and most outages happen at the gaps between them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Acceptance boundary.&lt;/strong&gt; Did the system actually take the work, or did the request return before the work was durably claimed?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue delivery boundary.&lt;/strong&gt; Did the message reach the worker, or did it get stuck, dropped, or duplicated?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistence boundary.&lt;/strong&gt; Once the worker processed the work, is the state durable and queryable?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery boundary.&lt;/strong&gt; If a worker dies, does another worker notice, and can it take over safely?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authoritative status boundary.&lt;/strong&gt; When something later asks &lt;em&gt;did this finish?&lt;/em&gt;, is there one place that knows?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can build a system that handles most of those well and still lose work. The last one is usually the one nobody writes down. I'll go boundary by boundary and show what I shipped at each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Acceptance: an atomic claim, not an OK response
&lt;/h3&gt;

&lt;p&gt;The first rule I wrote myself: never let an HTTP request return success before the work is durably claimed. An OK response is an acknowledgment that the request was received. It is not a claim.&lt;/p&gt;

&lt;p&gt;So the request lands in a Worker, and the Worker does one thing first. It opens a D1 transaction, inserts a task row with a &lt;code&gt;claimed_at&lt;/code&gt; timestamp, and only then returns. If the row insert fails, the request fails. If it succeeds, the work has a home before the queue is touched.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// acceptance boundary: claim the row before responding (illustrative shape)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`INSERT INTO tasks (id, kind, status, claimed_at, attempts)
     VALUES (?, ?, 'claimed', ?, ?)`&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialAttempts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`INSERT INTO task_events (id, task_id, kind, dedupe_key)
     VALUES (?, ?, 'enqueued', ?)`&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dedupe_key&lt;/code&gt; matters more than the row insert. If a client retries the same request (a webhook that fired twice, a UI that double-clicked, an upstream that timed out and replayed), the second attempt hits the unique constraint on &lt;code&gt;task_events.dedupe_key&lt;/code&gt; and aborts cleanly. No duplicate work. No &lt;em&gt;we sent two emails&lt;/em&gt; support tickets.&lt;/p&gt;

&lt;p&gt;The boundary test is simple: pull the power on the Worker between the HTTP response and the queue publish. If the task is recoverable from D1 alone, the acceptance boundary is honest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Queue delivery: one DLQ per logical queue
&lt;/h3&gt;

&lt;p&gt;A managed queue is durable, but durable is not the same as &lt;em&gt;always delivered&lt;/em&gt;. Retries run out. Messages expire. A poison payload can wedge a worker forever. If you only have one queue and one DLQ, you find out about the poison payload when the DLQ is full of unrelated retries.&lt;/p&gt;

&lt;p&gt;I split the queue namespace by what the work is doing, not by who produced it. Each logical queue gets its own DLQ:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="err"&gt;wrangler.toml:&lt;/span&gt; &lt;span class="err"&gt;queue&lt;/span&gt; &lt;span class="err"&gt;topology&lt;/span&gt; &lt;span class="err"&gt;(illustrative&lt;/span&gt; &lt;span class="err"&gt;shape)&lt;/span&gt;

&lt;span class="nn"&gt;[[queues.producers]]&lt;/span&gt;
&lt;span class="py"&gt;queue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"email"&lt;/span&gt;
&lt;span class="py"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"EMAIL_QUEUE"&lt;/span&gt;

&lt;span class="nn"&gt;[[queues.producers]]&lt;/span&gt;
&lt;span class="py"&gt;queue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ai"&lt;/span&gt;
&lt;span class="py"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"AI_QUEUE"&lt;/span&gt;

&lt;span class="nn"&gt;[[queues.producers]]&lt;/span&gt;
&lt;span class="py"&gt;queue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"webhook"&lt;/span&gt;
&lt;span class="py"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"WEBHOOK_QUEUE"&lt;/span&gt;

&lt;span class="nn"&gt;[[queues.producers]]&lt;/span&gt;
&lt;span class="py"&gt;queue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"github"&lt;/span&gt;
&lt;span class="py"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GITHUB_QUEUE"&lt;/span&gt;

&lt;span class="nn"&gt;[[queues.consumers]]&lt;/span&gt;
&lt;span class="py"&gt;queue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"email"&lt;/span&gt;
&lt;span class="py"&gt;dead_letter_queue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"email-dlq"&lt;/span&gt;
&lt;span class="py"&gt;max_retries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="err"&gt;retryBudget&lt;/span&gt;

&lt;span class="nn"&gt;[[queues.consumers]]&lt;/span&gt;
&lt;span class="py"&gt;queue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ai"&lt;/span&gt;
&lt;span class="py"&gt;dead_letter_queue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ai-dlq"&lt;/span&gt;
&lt;span class="py"&gt;max_retries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="err"&gt;retryBudget&lt;/span&gt;

&lt;span class="err"&gt;webhook&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;github&lt;/span&gt; &lt;span class="err"&gt;follow&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;same&lt;/span&gt; &lt;span class="err"&gt;shape&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;email-dlq&lt;/code&gt; starts filling, I know it is email-specific failure: bad templates, an upstream provider returning server errors, a malformed header. It is not a generic worker crash I have to chase. The DLQ is a signal, not a junk drawer. I drain it explicitly, and the act of draining is itself a D1 write so the inspection loop closes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Persistence: queues transport work, D1 stores truth
&lt;/h3&gt;

&lt;p&gt;The first version of this system kept status inside the queue message. That was a mistake. A queue message is a fact about now. Status is a fact about history. As soon as a worker needed to ask &lt;em&gt;did this finish yesterday?&lt;/em&gt;, the queue was useless.&lt;/p&gt;

&lt;p&gt;So the rule became: queues transport work, D1 stores truth. The schema is deliberately boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- persistence boundary: task state in D1 (illustrative shape)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;-- claimed | running | done | failed | dead&lt;/span&gt;
  &lt;span class="n"&gt;claimed_at&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;started_at&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;finished_at&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;tasks_status_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;tasks_kind_status_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;status&lt;/code&gt; column is intentionally an open enum. When I add a new pipeline stage I can introduce a new value without a migration. The &lt;code&gt;attempts&lt;/code&gt; column is the recovery counter the next boundary needs.&lt;/p&gt;

&lt;p&gt;What this buys me: a one-line SQL query tells me how many tasks are stuck in &lt;code&gt;claimed&lt;/code&gt; or &lt;code&gt;running&lt;/code&gt; past their expected duration. That is the dashboard. I don't need a tracing system, an APM, or a log search. The state is queryable, not inferred.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recovery: heartbeats and a periodic reset
&lt;/h3&gt;

&lt;p&gt;Workers die. That is the whole point of a queue. The question is what happens to the work they were holding when they died.&lt;/p&gt;

&lt;p&gt;A heartbeat column on the task row is the simplest mechanism that actually works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker: heartbeat while running (illustrative shape)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heartbeat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`UPDATE tasks
     SET last_heartbeat_at = ?
     WHERE id = ? AND status = 'running'`&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;heartbeatIntervalMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// illustrative cadence&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;doWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`UPDATE tasks SET status='done', finished_at=? WHERE id=?`&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heartbeat&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A separate recovery Worker runs on a schedule and asks one query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- recovery boundary: find rows whose worker stopped heartbeating (illustrative shape)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'running'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;last_heartbeat_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;   &lt;span class="c1"&gt;-- now minus heartbeat window&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;last_heartbeat_at&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of those rows gets reset to &lt;code&gt;claimed&lt;/code&gt; with &lt;code&gt;attempts = attempts + one&lt;/code&gt;. If &lt;code&gt;attempts&lt;/code&gt; exceeds the retry budget, the row goes to &lt;code&gt;dead&lt;/code&gt; and the DLQ catches the corresponding message. That is the orphan-task reset, and it is what stops a crashed worker from holding a job hostage.&lt;/p&gt;

&lt;p&gt;The auth boundary sits on the wake-up side. When the pipeline calls back, the request carries a bearer token, a pipeline identity, an HMAC signature over the body, and a timestamp with a small replay window. The Dispatcher (the small scheduling service sitting between the main app and the pipeline) verifies all four before doing anything. The verification is cheap. The discipline is mandatory. A callback without HMAC and timestamp is a webhook-shaped security hole.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authoritative status: the callback is a wake-up, not an answer
&lt;/h3&gt;

&lt;p&gt;The last boundary is the one I almost skipped. The pipeline sends a callback when it is done. I was about to trust that callback as the source of truth.&lt;/p&gt;

&lt;p&gt;I changed my mind. The callback is now treated as a wake-up signal, not a status update. The Dispatcher receives the callback, validates the auth, and immediately runs an authoritative query against the pipeline to ask &lt;em&gt;is this job actually done?&lt;/em&gt;. Only then does it mark the task &lt;code&gt;done&lt;/code&gt; in D1. If the authoritative query fails or times out, the task stays &lt;code&gt;running&lt;/code&gt; and the polling fallback takes over.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// callback handler: never trust the callback alone (illustrative shape)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handlePipelineCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyPipelineAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// bearer + id + HMAC + ts&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HttpStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Unauthorized&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;taskId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// wake the recovery loop&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requeueStatusCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// query the pipeline authoritatively&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;statusFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// mutate state only from the query result&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markDone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// else: leave it running; polling will catch up&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Polling is the fallback, not the strategy. It runs on a fixed cadence for any task in &lt;code&gt;running&lt;/code&gt; past its expected duration, and it runs the same authoritative query. If the callback was lost, polling catches it. If polling fails, the next heartbeat timeout resets the task and the worker retries. Three independent paths reach the same authoritative answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this design doesn't promise
&lt;/h3&gt;

&lt;p&gt;This architecture does not give me a delivery guarantee. The managed queue gives at-least-once delivery; my DLQs catch the rest, but they don't auto-recover. If &lt;code&gt;email-dlq&lt;/code&gt; fills up in the middle of the night, no one wakes me. I built alerts, not autonomy.&lt;/p&gt;

&lt;p&gt;It also doesn't replace tests. The atomic claim is correct only because the D1 transaction is correct. The heartbeat reset is correct only because the SQL is correct. I have caught bugs in every one of these boundaries during refactors. The architecture makes those bugs visible. It does not make them impossible.&lt;/p&gt;

&lt;p&gt;And none of this is novel. Atomic claim, idempotency keys, dead-letter queues, heartbeat recovery, callback-as-wake-up: these are patterns from the distributed-systems canon, transplanted into a Workers + D1 + Queues shape. The contribution isn't invention. It is the discipline of treating each boundary as something I can show the code for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I bother
&lt;/h3&gt;

&lt;p&gt;I am a solo founder. I don't have an SRE team. The only way my product survives contact with reality is if the architecture is something I can hold in my head and inspect late at night with one terminal open.&lt;/p&gt;

&lt;p&gt;Splitting async work into five inspectable boundaries (acceptance, queue delivery, persistence, recovery, authoritative status) gave me that. Each one has a table, a queue, a query, or a config block I can point at. When something goes wrong, I don't ask &lt;em&gt;what happened?&lt;/em&gt; I ask &lt;em&gt;which boundary?&lt;/em&gt;. The answer narrows the search to a few hundred lines.&lt;/p&gt;

&lt;p&gt;That is not a slogan. It is the reason this article exists at all. The code is the only proof I can offer without inventing a war story.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>cloudflarechallenge</category>
    </item>
  </channel>
</rss>
