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.

4 min readSume
All posts

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.

Timing limits that meet in one request; Heroku rows read 2026-10-06 from the Heroku Dev Center
LimitValueSource
Router timeout (H12)30 secondsHeroku
Rolling window after first byte55 secondsHeroku
Sume sync waitAt most 30 secondsSume jobs docs
Avatar clip renderOften longer than a web requestSume 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, 202

Where 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

All Developers posts

Written by Sume