Railway cron job that submits a Sume job and exits (Python)

Railway cron services must exit when done, skip a run while the last one is still going, and start at most every 5 minutes. Submit in webhook mode and exit.

4 min readSume
All posts

A Railway cron service should do one thing: submit the Sume job in webhook mode, print the job id and exit with code 0. Railway starts the service on the schedule, but it expects the process to terminate, and it will not start a new run while the previous one is still alive, so a script that waits on generation can silently suppress the following runs.

Treat the cron as a trigger, not a worker. The finished asset arrives at a separate webhook receiver, and the cron's only memory is the Idempotency-Key it derives from the time bucket.

Railway's cron rules, as documented

Railway's cron page says the schedule is evaluated in UTC, the minimum gap between runs is 5 minutes, and the service must exit when its task is complete, otherwise it is treated as still running. If a run is still going when the next is due, the new run is skipped. Start times can drift by a few minutes, so do not rely on a precise second.

Each of these maps to a decision. The 5-minute floor makes a Railway cron unsuitable for sub-minute polling, which Sume does not need anyway when a webhook delivers. The skip-on-overlap rule means a hung process costs you every following run, so set a request timeout on the submit and let the process end. The drift means the key should name a bucket, such as the hour, not a minute.

Railway cron rules and the matching script behavior (read 2026-10-03)
Railway ruleDocumented behaviorIn the Sume script
Time zoneUTCBuild the key from UTC
FrequencyAt least 5 minutes apartHourly or daily bucket
CompletionService must exit when doneSubmit, print, sys.exit
OverlapNew run skipped while the old one runsRequest timeout of 20 seconds
TimingMay vary by a few minutesBucket the key, not the minute

The script

Webhook mode returns a 202 immediately, so the process lives a second or two. A replay inside the same hour returns the first job, which makes a manual redeploy-triggered run harmless.

import os, sys, datetime, requests

bucket = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H")
r = requests.post(
    "https://api.sume.com/v1/images",
    headers={
        "Authorization": f"Bearer {os.environ['SUME_API_KEY']}",
        "Idempotency-Key": f"hourly-card-{bucket}",
    },
    json={
        "model": "sume/auto",
        "prompt": "Clean data-center photo, blue accent, no text",
        "mode": "webhook",
        "webhook_url": os.environ["SUME_HOOK_URL"],
    },
    timeout=20,
)
if r.status_code != 202:
    print("submit failed", r.status_code, r.text[:300])
    sys.exit(1)
d = r.json()["data"]
print("job", d["job"]["id"], "replay", d["idempotency_hit"])
sys.exit(0)

When the submit fails

Exiting non-zero marks the run failed in Railway, but nothing retries it until the next schedule. For a transient 429 or 503, add a short in-process retry with the same key, bounded well below the interval. For a 400, fail loudly: the prompt is wrong and the next run will fail the same way.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume