Cronhq - Cron jobs that actually run
by•
Cron jobs fail in silence. Two servers run the same crontab and your billing job fires twice. A job dies and nobody notices for weeks.
Cronhq is a scheduler built around one guarantee: exactly-once execution, enforced by Postgres locks — not best-effort. Around it: retries with backoff, HMAC-signed webhooks, heartbeat monitors for jobs we don't run, and alerts that fire once on failure and once on recovery.
Rust on Postgres. MIT-licensed, self-hostable, same image we run. Free tier: 5 jobs.


Replies
@michael_sunmisola To your second question: before trusting it with a job that moves money, I'd look for a run ID in the webhook headers.
Exactly-once covers your side. If two workers never fire the same slot, fine. Instead retries are a different story though: if my endpoint does the work and the response gets lost or times out, you call me again and the charge happens twice on my side. In the docs I only see X-Cronhq-Timestamp and X-Cronhq-Signature, and I guess the timestamp changes on every attempt, so I have nothing to dedupe on.
I hit this with Celery and Redis: long tasks got redelivered, ran twice, and I had to build the guard myself.
Is there an execution ID that stays the same across the retries of one run? Or is it planned?
This is very useful. This has been a bugbear of mine for a long time. Well done.
the "job runs twice" framing is exactly the failure mode that's easy to ignore until it hits a billing job specifically. Curious how the Postgres lock holds up under a worker crash mid-job - does the lock get released automatically so the next scheduled run can pick it up, or does a dead worker leave a job stuck until someone notices via the heartbeat alert?
@michael_sunmisola that split makes sense, the lock question was really "does it double run" and the honest answer is that's the smaller risk, the ambiguous-completion case is the scary one because nothing looks broken. is the run ID something CronHQ generates and passes to the endpoint so the endpoint itself can dedupe on it, or is it purely internal bookkeeping on your side right now? if it's the former that would let people build idempotency into the receiving job rather than trusting CronHQ's own retry logic alone.
@michael_sunmisola that's the right fix, and exposing it in the headers rather than keeping it internal is what actually lets people opt into real idempotency instead of just trusting the retry logic. one thing worth thinking through before it ships: will the header be additive (new header, old ones untouched) or will you be changing what X-Cronhq-Timestamp means, since anyone who already built a workaround off the existing headers would need a heads up either way. appreciate you thinking out loud about this in public, it's a good sign for how the product gets built.
@michael_sunmisola additive plus a separate attempt number is the right shape, that gives people the stable key for dedup and the attempt count for observability without overloading one field with two meanings. good luck with the ship - this has been one of the more useful threads I've followed on here, mostly because you kept answering the harder follow-up instead of stopping at the first good-enough answer.