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.

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).
| Topic | Heroku says | Design consequence |
|---|---|---|
| Frequencies | Every 10 minutes, every hour, or every day at a UTC time | Slot = UTC date or hour |
| Skipped jobs | Execution is expected but not guaranteed; a job may very rarely be skipped | Next run must check for a missing slot |
| Double runs | In very rare instances a job may run twice | Slot-based Idempotency-Key |
| Dyno lifetime | A scheduler dyno does not run longer than its interval; two dynos of the same job may briefly overlap | Submit and exit; do not poll inside the dyno |
| Recommendation | For critical jobs, run a custom clock process | Consider 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
- How many Format runs per minute can my Sume plan start?
Sume limits writes per minute by plan, from 120 on Free to 1200 on Scale, with reads at 40 times that. Read the rate-limit headers and back off on 429.
- HyperFrames check via the Sume API: caption collisions pre-render
Send check with caption_zone to POST /v1/hyperframes-previews and get findings, contrast and overlap reports. A failing check is still a completed job.
- image_not_fetchable on a Sume image edit: reference URL checklist
A Sume image edit failed with image_not_fetchable or input_media_unreachable. What the docs say the error means and a checklist for the reference URL.
- Image API returned 202, not an image: one Python handler for both
POST /v1/images waits 30 seconds, then returns a 202 job envelope. A Python handler that reads the status code, polls the job and returns image URLs either way.
Written by Sume