deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Cloudflare Workers pair with Modal GPUs for async Whisper transcription

A dev.to walkthrough details how ScribeToAny runs Whisper on Modal GPUs behind a Cloudflare Workers frontend, using presigned R2 uploads, fire-and-forget HTTP spawns and signed, idempotent webhooks.

Cloudflare Workers pair with Modal GPUs for async Whisper transcription

The constraint

Cloudflare Workers are built for short, request-scoped work: a Worker wakes on an incoming request, gets a modest CPU budget, and is expected to respond quickly and go away. According to a write-up on dev.to, that shape clashes head-on with what the author's transcription app, ScribeToAny, needs. Running Whisper on a ten-minute podcast is a multi-minute GPU job that a Worker simply cannot host, and the Worker cannot even start the job the normal way, because Modal's Python SDK reaches its platform over gRPC and Workers cannot hold that kind of persistent connection. The obvious design — await the transcription and return the transcript — fails immediately.

Treat Modal as an HTTP endpoint

The author's answer is to stop treating Modal as an SDK and treat it as a plain HTTP endpoint. Modal can expose a web endpoint that spawns the real GPU function and returns immediately, which collapses the whole integration into a handful of sub-second calls:

  • The browser uploads media straight to R2 using a presigned PUT URL, never streaming it through the Worker.
  • The Worker presigns a read URL for that object, writes a queued job row behind an atomic concurrency guard, and fires a single POST at Modal's web endpoint. Modal acknowledges with a call id and starts the GPU work in the background.
  • When the engine finishes, fails, or wants to report progress, it POSTs back to a webhook on the Worker, signed with a shared secret.
  • The frontend polls the job row and updates when it flips to done.

The spawn request authenticates with Modal's proxy auth scheme — a key and secret sent as headers rather than in the body — and carries the callback URL, so the GPU side never needs to know the app's topology. The webhook signing secret is pre-shared through Modal's secrets mechanism and never appears in a request body in either direction.

Uploads bypass the Worker entirely

Media never passes through the Worker, which would burn CPU for no benefit. The upload flow is intent-first: a pending row is written before the presigned URL is handed out, so an abandoned upload still leaves a trace that can be swept later. Once the client's PUT succeeds, it calls a finalize step that flips the row from pending to uploaded and corrects the file size from R2's HEAD metadata — the client-reported byte count is never trusted.

Verify, then apply idempotently

The webhook is where the interesting work happens. The engine signs the raw body with HMAC-SHA256 and sends the hex digest in an X-Webhook-Signature header; the Worker verifies it with WebCrypto (there is no Node crypto on Workers) using a timing-safe comparison. One pitfall the author highlights: the signature must be computed over the raw bytes. Re-serializing parsed JSON reorders keys and changes whitespace, so the signature will never match — the handler reads the raw text and verifies before it parses.

Because a webhook that is not acknowledged fast enough gets retried, applying results must be idempotent. The rule is one line: once a job reaches a terminal state such as done or failed, repeats are no-ops. HTTP status codes are chosen to steer the engine's retry behaviour. A bad or missing signature gets a 401 and outright rejection. An unknown job id gets a 200, treating it as a dead letter the sender should stop retrying rather than an error to bubble up. A successfully applied callback returns 200, and a failure in the Worker's own database returns 500 to ask the engine to try again.

Backstops for lost callbacks

The happy path is only about a third of the code, the author notes. If the engine crashes or a callback is dropped, a job would sit in transcribing forever, so a cron trigger reconciles state: jobs that have been quiet past a timeout are marked failed, and the same sweep collects orphaned R2 uploads that never finalized. A liveness probe can also ask Modal whether a call is still running, deliberately returning a three-valued answer — running, a terminal state, or null meaning the probe does not know.

Why it matters

Edge runtimes like Workers offer cheap, fast, globally distributed request handling but wall out long-running and GPU-bound work, and platform SDKs that assume gRPC or persistent sockets do not port to them. The pattern described here — direct-to-object-storage uploads, a fire-and-forget spawn over plain HTTP, signed webhooks with idempotent application, and cron-based reconciliation — bridges that gap without standing up queues or extra infrastructure. It is also a compact checklist of asynchronous-system hygiene: hash raw bytes before parsing, acknowledge dead letters so senders stop retrying, trust storage metadata over client claims, and assume every callback can arrive twice or not at all.

  • #cloudflare-workers
  • #modal
  • #serverless
  • #webhooks
  • #whisper
  • #gpu