Heroku H12 at 30 seconds: call Sume async, not sync
Heroku's router ends a request at 30 s (H12). Sume sync mode waits up to 30 s too. Submit async, return 202 to the browser, then poll or take a webhook.

Why does a sync Sume call hit H12?
Heroku's Dev Center says the router terminates a request after 30 seconds (error H12), and the dyno keeps processing it anyway. There is also a rolling 55 second window after the first byte. Sume's sync mode waits at most 30 seconds for a job, so a dyno that submits and waits has no budget left for its own work, and a slower job returns a non-terminal envelope.
Heroku's own advice is to move long work to a background task and poll for it.
| Limit | Value | Source |
|---|---|---|
| Router timeout (H12) | 30 seconds | Heroku |
| Rolling window after first byte | 55 seconds | Heroku |
| Sume sync wait | At most 30 seconds | Sume jobs docs |
| Avatar clip render | Often longer than a web request | Sume avatar docs |
What is the safe pattern?
Leave mode unset (async is the default). The request returns a 202 with a job envelope that has status_url, result_url and events_url. Hand the job id to the browser and return at once. The page then polls your endpoint, which reads GET /v1/jobs/:id/status and obeys next_poll_after_seconds when it is present.
Do not resubmit the paid request because your own call timed out. Poll the job you already have.
import json, os, urllib.request
def post(path, body, key):
req = urllib.request.Request(
"https://api.sume.com" + path,
data=json.dumps(body).encode(),
headers={
"Authorization": "Bearer " + os.environ["SUME_API_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": key,
},
)
with urllib.request.urlopen(req) as r:
return json.load(r)
job = post("/v1/avatar-1.0/talking-video", {
"avatar_handle": "your_avatar_handle",
"script": "Your order shipped. Here is what to expect.",
}, "order-1001-shipped-clip")
print(job["status_url"]) # return this id to the browser, 202Where does a webhook beat polling?
On a dyno, a webhook means no polling loop to keep alive. Sume signs it with HMAC-SHA256 over the timestamp and body, and the docs set a 5 minute replay tolerance; verify before acting. The Netlify version of this problem is in the background function post.
Sources
Related posts
More in Developers
- How Sume TTS splits a script into sentence ids (. ! ? only)
Sume's script source cuts sentences at periods, exclamation and question marks only, keeps every character, and numbers them sent_000000. Examples and traps.
- Huey task that polls an AI video job with schedule(delay=...)
A Huey task reads Sume's job status once and reschedules itself with task.schedule(delay=...) from next_poll_after_seconds, so no worker thread sleeps.
- Idempotency key for transcription: hash the request, avoid 409
Derive the Sume STT Idempotency-Key from a hash of audio_url, language and duration. A retry returns the same job. A changed body under that key is a 409.
- Ideogram 4.5 low to high on one order: new Idempotency-Key or a 409
A reused Idempotency-Key with a different payload returns 409 idempotency_conflict. Key on order id plus a payload hash so a quality change is a new job.
Written by Sume