cd /news/developer-tools/building-a-reliable-ai-transcription… · home topics developer-tools article
[ARTICLE · art-112724] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Building a Reliable AI Transcription Pipeline on Cloudflare Workers

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.

read4 min views1 publishedAug 27, 2026

I recently shipped HiTranscript, a web app that turns public video URLs and local media uploads into searchable transcripts and subtitle files.

The transcription model was not the hardest part.

The 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.

This 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.

The 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.

Browser
  |
  v
TanStack Start API
  |
  +--> PostgreSQL task record
  |
  +--> private R2 object
  |
  v
Cloudflare Queue
  |
  v
media preparation / transcription worker
  |
  v
signed callback
  |
  v
normalized word timeline
  |
  +--> readable transcript
  +--> subtitle cues
  +--> TXT / DOCX / PDF
  +--> SRT / WebVTT

The 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.

A boolean such as isProcessing is not enough for a media pipeline.

A 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.

A simplified state model looks like this:

type TranscriptStatus =
  | "awaiting_media"
  | "media_preparing"
  | "queued"
  | "processing"
  | "completed"
  | "failed";

The value of explicit states is not the union type itself. The value is being able to define valid transitions.

For example:

When every write checks the previous state, stale workers cannot move a finished task backward.

Passing large audio or video bodies through several HTTP requests creates unnecessary failure points.

The safer pattern is:

The queue message should describe work, not carry the work itself.

This also keeps the web process responsive. The browser can display upload progress, while the backend independently reports preparation and transcription progress.

Retries 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.

The callback handler therefore has to be idempotent.

A simplified version of the rule is:

async function completeTask(input: CompletionPayload) {
  verifySignature(input);

  const task = await findTask(input.taskId);

  if (task.status === "completed") {
    return task;
  }

  return database.transaction(async (tx) => {
    const updated = await tx.updateTaskWhereStatus({
      taskId: input.taskId,
      expected: ["queued", "processing"],
      next: "completed",
      result: normalizeResult(input.result),
    });

    if (!updated) {
      return findTask(input.taskId);
    }

    await settleBillingOnce(tx, input.taskId);
    return updated;
  });
}

The 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.

Signed callbacks are equally important. Idempotency prevents accidental duplication; signature verification prevents unauthorized state changes.

It 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.

A more useful model is:

This makes partially_completed a first-class outcome rather than an exception.

It 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.

A transcript paragraph, an SRT file, and a short-form caption layout are different views of the same timing data.

Instead of storing only a large text blob, the pipeline keeps a normalized word timeline:

type TimedWord = {
  text: string;
  startMs: number;
  endMs: number;
  speakerId?: string;
};

From that timeline, the application can derive:

This avoids running transcription again when the user changes an output option. It also keeps every view aligned to the same source data.

The 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.

The public contract should expose stable, actionable categories such as:

Internally, retain the detailed diagnostic code and attempt history. Externally, show a message the user can act on.

This separation also lets you change providers without changing the product's error language.

For this kind of pipeline, deployment safety matters as much as code correctness.

My preferred release flow is:

Rebuilding between validation and promotion breaks the evidence chain. The artifact that reaches users should be the artifact that was tested.

The next reliability gains are less about adding more providers and more about strengthening the boundaries:

A successful provider response is not the same as a usable transcript. Output shape, timestamp coverage, language behavior, and retry semantics all need validation.

An AI transcription product is a distributed media system before it is an AI demo.

The 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.

If 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.

What has been the hardest reliability problem in your own asynchronous pipeline?

── more in #developer-tools 4 stories · sorted by recency
── more on @hitranscript 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/building-a-reliable-…] indexed:0 read:4min 2026-08-27 ·