Heroku Scheduler can skip or double-run: protect a Sume job

Heroku says Scheduler may miss a run or run twice. Use a slot Idempotency-Key plus a catch-up check so Sume bills once per slot.

4 min readSume
All posts

Heroku Scheduler is documented as best effort: a job can occasionally be skipped or run twice. For a Sume call that means two things: send an Idempotency-Key built from the slot so a double run returns the same job, and add a catch-up step so a skipped slot is submitted by the next run.

What Heroku documents

Facts below are from the Heroku Dev Center page (read 2026-10-10).

Heroku Scheduler reliability, from devcenter.heroku.com/articles/scheduler (read 2026-10-10)
TopicHeroku saysDesign consequence
FrequenciesEvery 10 minutes, every hour, or every day at a UTC timeSlot = UTC date or hour
Skipped jobsExecution is expected but not guaranteed; a job may very rarely be skippedNext run must check for a missing slot
Double runsIn very rare instances a job may run twiceSlot-based Idempotency-Key
Dyno lifetimeA scheduler dyno does not run longer than its interval; two dynos of the same job may briefly overlapSubmit and exit; do not poll inside the dyno
RecommendationFor critical jobs, run a custom clock processConsider a clock dyno if a missed slot is costly

Submit with the slot key

Run this as the scheduler command, for example python submit.py. The fields come from the Image 1.0 request table. The key uses the UTC hour, so a double run in the same hour collapses into one Sume job.

import json, os, urllib.request
from datetime import datetime, timezone

now = datetime.now(timezone.utc)
slot = now.strftime("%Y-%m-%dT%H")
body = {
    "prompt": "Soft daylight product shot on a linen cloth",
    "quality": "low",
    "aspect_ratio": "4:5",
    "mode": "webhook",
    "webhook_url": "https://example.com/hooks/sume",
}
req = urllib.request.Request(
    "https://api.sume.com/v1/image-1.0/generate",
    data=json.dumps(body).encode(),
    headers={
        "Authorization": "Bearer " + os.environ["SUME_API_KEY"],
        "Content-Type": "application/json",
        "Idempotency-Key": "nightly-hero-" + slot,
    },
)
with urllib.request.urlopen(req, timeout=20) as r:
    print(r.status, r.read().decode())

Catch up on a skipped slot

Because Scheduler may skip, store each slot's job id in your database when the webhook arrives. At the start of every run, look at the previous slot. If it has no job id, submit it first with its own key. Sume job reads (GET /v1/jobs) are limited to the jobs your key created, so your own table is the record of which slots are done.

Why a webhook and a slot key together

Submit in webhook mode so the scheduled process can exit. Sume sends only terminal events (job.completed, job.failed, job.canceled) and retries up to 10 attempts, 30 seconds apart, with a 10 second timeout for each attempt, per the Webhooks page. Use job_id as the idempotency key on your receiver.

Delivery is an optimization, not the only recovery path. Keep GET /v1/jobs/:id/status polls available for events that never arrive. A repeat of the same key with the same body is an exact retry. A repeat with a different body returns 409 idempotency_conflict, as the Generation admission page lists.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume