{"slug": "building-a-reliable-ai-transcription-pipeline-on-cloudflare-workers", "title": "Building a Reliable AI Transcription Pipeline on Cloudflare Workers", "summary": "A developer has built HiTranscript, a web app that converts public video URLs and local media uploads into searchable transcripts and subtitle files, and detailed the architecture patterns that ensure reliability. The system uses explicit job states, durable media handoffs, idempotent callbacks, item-level batch tracking, and a normalized timeline, built on Cloudflare Workers, Queues, and R2 with PostgreSQL for task state.", "body_md": "I recently shipped [HiTranscript](https://hitranscript.com/), a web app that turns public video URLs and local media uploads into searchable transcripts and subtitle files.\n\nThe transcription model was not the hardest part.\n\nThe hard part was building a pipeline that stays correct when uploads are large, requests are retried, callbacks arrive twice, a batch partially fails, or a deployment needs to be rolled back.\n\nThis post covers the architecture patterns that made the system more reliable: explicit job states, durable media handoffs, idempotent callbacks, item-level batch tracking, and a single normalized timeline for every output format.\n\nThe application uses TanStack Start and TypeScript for the web layer, PostgreSQL for durable task state, and Cloudflare Workers, Queues, and R2 for orchestration and media storage.\n\n``` php\nBrowser\n  |\n  v\nTanStack Start API\n  |\n  +--> PostgreSQL task record\n  |\n  +--> private R2 object\n  |\n  v\nCloudflare Queue\n  |\n  v\nmedia preparation / transcription worker\n  |\n  v\nsigned callback\n  |\n  v\nnormalized word timeline\n  |\n  +--> readable transcript\n  +--> subtitle cues\n  +--> TXT / DOCX / PDF\n  +--> SRT / WebVTT\n```\n\nThe important design decision is that the HTTP request does not try to finish the transcription. It only validates the request, persists the intent, and creates a durable handoff.\n\nA boolean such as isProcessing is not enough for a media pipeline.\n\nA real task can be waiting for media, preparing media, queued for transcription, actively processing, completed, or failed. Each state has different retry and UI behavior.\n\nA simplified state model looks like this:\n\n```\ntype TranscriptStatus =\n  | \"awaiting_media\"\n  | \"media_preparing\"\n  | \"queued\"\n  | \"processing\"\n  | \"completed\"\n  | \"failed\";\n```\n\nThe value of explicit states is not the union type itself. The value is being able to define valid transitions.\n\nFor example:\n\nWhen every write checks the previous state, stale workers cannot move a finished task backward.\n\nPassing large audio or video bodies through several HTTP requests creates unnecessary failure points.\n\nThe safer pattern is:\n\nThe queue message should describe work, not carry the work itself.\n\nThis also keeps the web process responsive. The browser can display upload progress, while the backend independently reports preparation and transcription progress.\n\nRetries are normal in distributed systems. A callback may arrive twice because a worker timed out after completing the request, a queue retried the message, or the provider repeated a webhook.\n\nThe callback handler therefore has to be idempotent.\n\nA simplified version of the rule is:\n\n```\nasync function completeTask(input: CompletionPayload) {\n  verifySignature(input);\n\n  const task = await findTask(input.taskId);\n\n  if (task.status === \"completed\") {\n    return task;\n  }\n\n  return database.transaction(async (tx) => {\n    const updated = await tx.updateTaskWhereStatus({\n      taskId: input.taskId,\n      expected: [\"queued\", \"processing\"],\n      next: \"completed\",\n      result: normalizeResult(input.result),\n    });\n\n    if (!updated) {\n      return findTask(input.taskId);\n    }\n\n    await settleBillingOnce(tx, input.taskId);\n    return updated;\n  });\n}\n```\n\nThe database transition, result persistence, and billing settlement belong to one consistency boundary. A duplicate callback should return the existing result instead of charging twice or creating a second output.\n\nSigned callbacks are equally important. Idempotency prevents accidental duplication; signature verification prevents unauthorized state changes.\n\nIt is tempting to model a batch as one task containing an array of URLs. That becomes painful when one item fails and the other 49 succeed.\n\nA more useful model is:\n\nThis makes partially_completed a first-class outcome rather than an exception.\n\nIt also improves fairness. A scheduler can take a capacity snapshot and dispatch work across batches instead of letting one large batch block every single-item request.\n\nA transcript paragraph, an SRT file, and a short-form caption layout are different views of the same timing data.\n\nInstead of storing only a large text blob, the pipeline keeps a normalized word timeline:\n\n```\ntype TimedWord = {\n  text: string;\n  startMs: number;\n  endMs: number;\n  speakerId?: string;\n};\n```\n\nFrom that timeline, the application can derive:\n\nThis avoids running transcription again when the user changes an output option. It also keeps every view aligned to the same source data.\n\nThe internal error may say that a decoder failed, a queue exhausted its retries, or an upstream service rejected a media file. That detail is useful in logs but often harmful in the UI.\n\nThe public contract should expose stable, actionable categories such as:\n\nInternally, retain the detailed diagnostic code and attempt history. Externally, show a message the user can act on.\n\nThis separation also lets you change providers without changing the product's error language.\n\nFor this kind of pipeline, deployment safety matters as much as code correctness.\n\nMy preferred release flow is:\n\nRebuilding between validation and promotion breaks the evidence chain. The artifact that reaches users should be the artifact that was tested.\n\nThe next reliability gains are less about adding more providers and more about strengthening the boundaries:\n\nA successful provider response is not the same as a usable transcript. Output shape, timestamp coverage, language behavior, and retry semantics all need validation.\n\nAn AI transcription product is a distributed media system before it is an AI demo.\n\nThe durable design comes from treating state transitions, storage handoffs, callbacks, billing, and deployment artifacts as explicit contracts. Once those boundaries are reliable, switching models or adding output formats becomes much less risky.\n\nIf you are building a similar workflow, I would start with the state machine and idempotency rules before optimizing model latency. Those two decisions will shape almost every failure you have to handle later.\n\nWhat has been the hardest reliability problem in your own asynchronous pipeline?", "url": "https://wpnews.pro/news/building-a-reliable-ai-transcription-pipeline-on-cloudflare-workers", "canonical_source": "https://dev.to/bill_king_d4cd78085ee37d2/building-a-reliable-ai-transcription-pipeline-on-cloudflare-workers-5hie", "published_at": "2026-08-27 05:26:21+00:00", "updated_at": "2026-08-27 05:47:57.931712+00:00", "lang": "en", "topics": ["developer-tools", "ai-products", "ai-infrastructure", "mlops"], "entities": ["HiTranscript", "Cloudflare Workers", "Cloudflare Queues", "Cloudflare R2", "TanStack Start", "PostgreSQL"], "alternates": {"html": "https://wpnews.pro/news/building-a-reliable-ai-transcription-pipeline-on-cloudflare-workers", "markdown": "https://wpnews.pro/news/building-a-reliable-ai-transcription-pipeline-on-cloudflare-workers.md", "text": "https://wpnews.pro/news/building-a-reliable-ai-transcription-pipeline-on-cloudflare-workers.txt", "jsonld": "https://wpnews.pro/news/building-a-reliable-ai-transcription-pipeline-on-cloudflare-workers.jsonld"}}