Developer Guides5 min read

Async Transcription with Webhooks: How to Process Audio at Scale Without Polling

I've shipped async transcription pipelines handling 800+ hours of audio daily. Here's the webhook architecture, retry logic, and idempotency that work at scale.

Async Transcription with Webhooks: How to Process Audio at Scale Without Polling

I've shipped async transcription pipelines that handle 800+ hours of audio a day for AI agent platforms, and I can tell you exactly when polling breaks. It's around the 200-concurrent-job mark. Your workers spend more time checking job status than doing real work, your API bill quietly doubles, and a single misconfigured retry loop will keep an entire pod pool busy for hours. Webhooks fix this if you implement them with the right retry semantics and idempotency. In this guide, I'll show you the webhook architecture I've used for seven production deployments.

In our complete guide to speech-to-text for AI agents, we covered how the voice pipeline fits together. This cluster zooms in on the async handoff — the part where most teams start losing time to infrastructure problems they didn't see coming.

Why polling stops working at scale

Polling is the default for a reason — it works fine with a handful of jobs. Submit a transcription, sleep for five seconds, ask if it's done, repeat. The latency cost is invisible at low volume.

The math changes the moment you go parallel. With 500 jobs in flight and a 2-second poll, you hit the status endpoint 250 times per second doing nothing productive. On Deepgram and AssemblyAI you can absorb that with per-request pricing, but the same pattern against an internal service will saturate a connection pool before lunch.

The webhook receiver is what finally worked for every team I've worked with. The right answer is to never ask twice.

How webhook transcription actually works

A webhook flow inverts the timing. Your application submits the audio file once, gets back a job ID immediately, and returns to whatever it was doing. The transcription API processes the audio on its own schedule, and when the job finishes, it sends an HTTP POST to a URL you registered.

The sequence:

  • Submit job — POST the audio file or URL with a webhook_url parameter
  • Receive job ID — the API returns immediately
  • Process asynchronously — the API works the job in its own queue
  • Deliver webhook — on completion, the API POSTs a JSON payload to your handler
  • Acknowledge with 200 — your handler validates the signature, persists the result, returns 2xx

The OpenAI Whisper API and most enterprise transcription services follow this pattern. Privocio's async endpoint exposes a webhook_url parameter that posts to your handler when the job finishes.

Here's the Flask pattern I keep coming back to:

import hmac, hashlib
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = b"your-shared-secret-here"

@app.post("/transcription-callback")
def receive():
    signature = request.headers.get("X-Webhook-Signature", "")
    body = request.get_data()
    expected = hmac.new(WEBHOOK_SECRET, body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(signature, expected):
        abort(401)
    persist_result(request.get_json())
    return "", 200

The compare_digest line matters. It runs in constant time, which means a timing-attack adversary can't measure how close their guess was to the real signature.

The five things every webhook handler needs

Most teams treat webhooks as "just receive and store," and then they get surprised when things go wrong. Here's the checklist I run:

  • Signature verification on every request — never trust a payload that doesn't pass the HMAC check
  • Idempotency by job ID — providers retry on non-2xx responses, so the same job can arrive three or four times
  • Quick acknowledgment, slow processing — return 200 within a few hundred milliseconds, do heavy work in a background queue
  • Structured logging with the job ID — when something breaks, you need to trace one job through the system
  • Dead-letter handling — after N retry attempts, persist the failed payload somewhere a human can look at it

Idempotency is the one that always catches people off guard. The transcription API doesn't know whether your handler is idempotent. It just knows it didn't get a 2xx back in 30 seconds, so it retries. If your handler appends to a transcript table without checking, you'll have three copies of the same row by the end of the day.

The fix is one SQL statement:

INSERT INTO transcripts (job_id, status, body, created_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (job_id) DO UPDATE
SET status = EXCLUDED.status,
    body = EXCLUDED.body,
    updated_at = NOW();

That single statement handles retries, status changes, and partial updates.

Retry strategies that survive production

Webhook delivery is not guaranteed. Networks fail, your handler pod restarts, the database goes down. A production-grade API will retry failed deliveries with exponential backoff, and you need to design for that.

The pattern I use most often is the "ack-fast, process-async" split. The handler validates the signature, writes the payload to a durable queue, and returns 200. A separate worker pool consumes the queue. Even if your downstream system is having a bad day, the API gets its 200 and stops retrying.

Failure TypeAPI Retry BehaviorYour Recovery Action
Handler returns 5xxRetry with exponential backoff (1s, 2s, 4s, 8s, 16s, 32s)Investigate logs, replay from dead-letter queue
Handler times out (no response in 30s)Treat as 5xx, retry per backoff scheduleOptimize handler to ack in under 500ms
Handler returns 4xxStop retrying — payload is malformedLog to error tracking, alert on rate
Network failure between API and handlerRetry per backoff scheduleVerify DNS and TLS certificate
Job exceeds max retry countMove to dead-letter queue at API levelManual review and replay

The cleanest approach for idempotency is to use the job ID itself as the natural key. The database constraint does the work.

A silent failure looks identical to a healthy pipeline — your app just stops getting transcripts. I add a heartbeat job to every deployment that fires an alert if no webhooks arrive in a 15-minute window during expected traffic.

If you're building this for the first time, start with Privocio's webhook transcription endpoint. The fixed-pricing model means you can process 400 hours of audio per 4-week cycle for $19 flat, giving you room to experiment with retry and idempotency logic without watching a usage meter.

Frequently Asked Questions

How long should my webhook handler take to respond?

Return 2xx within 500 milliseconds if at all possible. Most transcription APIs time out at 30 seconds and treat slow responses as failures, triggering retries. The cleanest pattern is to write the payload to a queue, return 200, and process asynchronously.

What happens if my endpoint is down during a delivery?

The API will retry with exponential backoff. Most providers retry 5-7 times over 24 hours. After the final failure, the job lands in a dead-letter queue and you can replay it manually.

Do I need HTTPS for my webhook URL?

Yes. Every production transcription API I work with refuses plain HTTP. For local development, ngrok or a similar tunneling tool works fine.

How do I test webhooks locally?

Use a tool like ngrok to expose localhost, then point your webhook URL at the ngrok URL. Most providers also let you trigger a test delivery from the dashboard.

Should I use the same endpoint for multiple providers?

You can, but I don't recommend it. Each provider signs payloads differently, and mixing them in one handler makes the signature verification code harder to reason about. A small router that dispatches by URL path is cleaner.

Conclusion: webhooks are not optional at scale

If you're processing more than 50 audio hours a day, polling will eventually cost you more in infrastructure than the transcription itself. I've seen teams burn $800 a month on a status-check endpoint that webhook delivery would have eliminated. The migration is straightforward — submit with a webhook URL, validate signatures, ack fast, process async — and the same pattern handles 10x the volume without code changes.

If you're building a voice pipeline for an AI agent, start with Privocio's webhook-based transcription API and the full voice pipeline guide.


Image Credits:

Cover image sourced from Unsplash (Unsplash License).

speech-to-textjavascriptpythonlangchain
Async Transcription Webhooks: Scale Audio Processing | Privocio