Know the moment a cron job stops running.
Open-source monitoring for cron jobs, scheduled tasks and background workers. Add one line to a job. When it misses a run, fails or runs too long, you get a message on Telegram, Slack or email.
Checks every schedule every 15 seconds.
0 2 * * * ./close-month.sh \ && curl -fsS $PING_URL
@Scheduled(cron = "0 0 3 * * *")
void backupInvoices() { … }
close-month is due at 02:00. Grace period 10 min.
close-month missed its 02:00 run. Last success Oct 9, 38 s.
close-month is back. Ran at 02:14 in 38 s.
Fourteen nights of one job.
Each bar is one run and its height is how long it took. When tonight's run does not show up within the grace period, the job turns red and you get one message. When it runs again, you get one more.
close-month missed its 02:00 run. Last success Oct 9 at 02:00, 38 s.
close-month is back up. Ran at 02:14 and finished in 38 s.
Add one line to the job you already have.
Each job gets its own ping URL. The job calls it when it finishes, or calls /start and /fail too if you want durations and errors. When a ping does not arrive on schedule, you hear about it.
- Nothing to install where the job runs. If it can make an HTTP request, it works.
- Schedules are real cron expressions with a time zone, so a 02:00 job in Vietnam time is checked at 02:00 Vietnam time.
- Already on Healthchecks.io? The ping URLs have the same shape. Change the host and you are done.
# Nightly backup, then tell Cronbell it worked 0 2 * * * /opt/backup.sh && \ curl -fsS -m 10 --retry 3 https://cronbell.example.com/ping/5b1f0c2e
URL=https://cronbell.example.com/ping/5b1f0c2e-… curl -fsS -m 10 "$URL/start" if pg_dump app > /backups/app.sql 2> err.log; then curl -fsS -m 10 "$URL" else curl -fsS -m 10 --data-binary @err.log "$URL/fail" fi
import urllib.request PING = "https://cronbell.example.com/ping/5b1f0c2e-…" send_daily_report() urllib.request.urlopen(PING, timeout=10)
const PING = "https://cronbell.example.com/ping/5b1f0c2e-…"; await fetch(`${PING}/start`); try { await syncInvoices(); await fetch(PING); } catch (e) { await fetch(`${PING}/fail`, { method: "POST", body: String(e) }); }
apiVersion: batch/v1 kind: CronJob spec: schedule: "0 2 * * *" timeZone: Asia/Ho_Chi_Minh jobTemplate: spec: template: spec: containers: - name: close-month image: billing:1.42 command: ["sh", "-c", "./close-month && wget -qO- $PING_URL"]
Four ways a job goes wrong, and what you see.
Every alert says which job, what happened, when it was expected in the job's own time zone, and the last thing it printed.
- MISSED
close-month · no ping within 10 min of the 02:00 run
The job never started: a deploy dropped the schedule, the server was down, or a lock was left behind.
- FAILED
db-backup · exit 1 · pg_dump: connection to server failed
The job ran and reported an error. The last lines of its output come with the alert.
- RUNNING TOO LONG
sync-shopee-orders · started 03:00, still running after 45 min
The job started and never finished. Nothing failed, so nothing else would have told you.
- RECOVERED
close-month · ran at 02:14, finished in 38 s
One message when it breaks and one when it is fixed. No reminder every minute.
Every job, its schedule and its last run on one screen.
A preview of the dashboard we are building. The bars show how long the last runs took, so a job that is slowly getting slower stands out before it breaks.
| Job | Schedule | Last run | Next due | Duration, last 12 | Status |
|---|---|---|---|---|---|
| close-monthbilling-api | 0 2 * * * | Oct 9, 02:00 · 38 s | overdue 10 min | DOWN | |
| db-backupops · pg_dump to S3 | 0 3 * * * | Oct 10, 03:00 · 4 m 12 s | Oct 11, 03:00 | UP | |
| sync-shopee-orders-and-refunds-for-all-storesworker · every 15 min | */15 * * * * | 10:45 · 1 m 03 s | 11:00 | UP | |
| InvoiceJob.sendRemindersSpring @Scheduled · found automatically | 0 0 9 * * MON-FRI | Oct 10, 09:00 · 6 s | Oct 13, 09:00 | UP | |
| renew-certificatescertbot · weekly | 0 4 * * 0 | — | Oct 12, 04:00 | no runs yet | NEW |
| cleanup-temp-filespaused during migration | 0 * * * * | Oct 7, 13:00 · 2 s | — | paused | PAUSED |
Running on your own server in a minute.
One container, one port. It stores everything in a single SQLite file by default and switches to PostgreSQL when you set a database URL. Your job names, schedules and logs never leave your network.
- Free and complete. Nothing is held back for the cloud version.
- Fits on a small VPS: the goal is under 300 MB of memory for 1,000 jobs.
- Images for amd64 and arm64, so a Raspberry Pi works too.
docker run -d --name cronbell \ -p 8080:8080 \ -v cronbell-data:/data \ ghcr.io/steelreed/cronbell
services: cronbell: image: ghcr.io/steelreed/cronbell ports: ["8080:8080"] environment: CRONBELL_DATABASE_URL: postgres://cronbell:secret@db:5432/cronbell depends_on: [db] db: image: postgres:17 environment: { POSTGRES_USER: cronbell, POSTGRES_PASSWORD: secret }
On Spring Boot, it finds your jobs for you.
Add the starter and set one key. At startup it registers every @Scheduled, Quartz and ShedLock job with its real schedule, and reports every run with its duration and exception. You do not touch the job code.
Reporting runs in the background with a bounded queue. If the monitor is unreachable, your jobs carry on as if it were not there.
@Component class InvoiceJob { // Found at startup as "InvoiceJob.sendReminders", // schedule 0 0 9 * * MON-FRI, Asia/Ho_Chi_Minh @Scheduled(cron = "0 0 9 * * MON-FRI", zone = "Asia/Ho_Chi_Minh") void sendReminders() { invoices.overdue().forEach(mailer::remind); } } # application.yml cronbell: url: https://cronbell.example.com key: ${CRONBELL_KEY}
How it compares.
Good tools already exist, and some are open source too. Prices are from their websites in October 2026.
| Tool | Open source | Self-hosted | Cron schedules with time zones | Run durations and errors | Alerts when a job never ran | Finds Spring jobs without code changes |
|---|---|---|---|---|---|---|
| Healthchecks.io20 checks free · $20/mo for 100 | ||||||
| Cronitor$2 per monitor + $5 per user /mo | SDK, add calls by hand | |||||
| Uptime KumaFree | interval heartbeat only | |||||
| Sentry Crons$0.78 per monitor /mo | source-available | with all of Sentry | annotate each job | |||
| A script that emails youFree | — | — | only if it runs |
Cronbell is in development: its row describes what the first release is built to do. Spotted something wrong about another tool? Tell us at hello@steelreed.com and we will fix it.
Free to run yourself. Cheap when we run it.
The cloud plans are planned prices for launch and may still change. People who sign up for early access get the first months of Team free.
Self-hosted
$0
On your server, for as long as you like.
- Unlimited jobs and members
- Every alert channel
- History as long as you keep it
- Help through GitHub issues
Cloud Free
$0
For side projects and small servers.
- 20 jobs
- Email and Telegram
- 30 days of history
- 1 member
Cloud Team
$9/month
For a team that runs production jobs.
- 200 jobs
- Slack, webhooks, and Zalo later
- 1 year of history and duration trends
- 10 members, email support
Get early access.
The first build goes to people on this list, for self-hosting or on the cloud. Tell us how you run scheduled jobs today and we will shape the first version around it.
Questions
Can I use it today?
Not yet. It is in development and the first build goes to the early access list. The source code goes public on GitHub with that build.
How is it different from Healthchecks.io?
Healthchecks.io is excellent and open source too, and we use the same ping URL shape so moving is easy. We focus on three things: self-hosting with one Docker command and SQLite, a dashboard that shows duration trends, and a Spring Boot starter that finds jobs without code changes.
What happens if the monitor itself goes down?
Your jobs keep running: a ping that fails is ignored by the job. When the monitor comes back it does not alert for runs it could not see while it was down. On the cloud we watch the monitor from a separate provider.
Which licence is it under?
The server and dashboard are AGPL-3.0. The Spring Boot starter and client libraries are Apache-2.0, so adding them never changes the licence of your application.
Who builds it?
Steelreed, a small studio in Vietnam. We also make QueryFence, which checks the SQL your tests send to the database.