How cronpad works
cronpad is a single Go binary that serves the dashboard, the API, the MCP server, and also runs the scheduler. It keeps durable state in two SQLite files and a third, rebuildable SQLite file as the queue of what to run next. Nothing else needs to run. A single dispatcher goroutine pulls due work out of the queue and hands it to a pool of workers that deliver webhooks or run commands, recording every attempt.
The public website and documentation are a separate deployment at cronpad.com. The hosted dashboard runs at app.cronpad.com; self-hosted installations serve it at the root of their configured public URL.
Components
┌──────────────────────────── cronpad (one binary, one port) ─────────────────────────┐
Browser ─────▶ │ / dashboard │
API / CLI ───▶ │ /api/v1 REST API /auth Google, GitHub sign-in │
AI agents ───▶ │ /mcp MCP server │
│ │
│ claim loop ──claim──▶ queue: due, inflight (SQLite) ──▶ workers ──▶ webhook/command│
│ ▲ │ │
│ reconciler, reaper result writer, notifier ──▶ alerts
└─────┬───────────────────────────────────────────────────────┬──────────────────────┘
│ │
cronpad.db (jobs, users, config) history.db (runs, attempts, audit)
└─────────── Litestream ──▶ S3 / GCS / Azure / SFTP ─────┘
| Component | What it does |
|---|---|
| HTTP server | Dashboard, REST API, MCP server, OAuth sign-in and health checks. Works even while the scheduler is starting. |
| Claim loop | One goroutine. Atomically claims due occurrences from the queue and hands them to workers. Only runs while it holds the dispatcher lock. |
| Workers | A pool (64 by default) that executes sinks, classifies results, schedules retries and records attempts. |
| Result writer | Commits database writes as soon as it is free, grouping whatever arrives meanwhile (up to 500 commands) into one transaction, so the hot path never waits on many small transactions. |
| Reaper | Returns occurrences whose worker stopped renewing its lease (crash, hang) to the queue. |
| Reconciler | Makes sure the queue matches the databases: repairs lost entries, rebuilds a deleted or replaced queue file, handles missed runs after downtime. |
| Notifier | Evaluates alert rules and sends email, Slack, Discord or webhook alerts from a durable outbox. |
| Janitor | Deletes old history according to the retention policy and maintains daily rollups. |
The life of one run
Take a job nightly-report scheduled every day at 9am in Asia/Kolkata.
- Create. The API validates the schedule, computes the next fire time (09:00 IST = 03:30 UTC), saves the job in
cronpad.dbwithnext_run_at, and adds an entry to the queue’sduetable at 03:30 UTC. - Claim. At 03:30:00 the claim loop wakes (it sleeps exactly until the earliest due time), and one transaction moves the entry from
duetoinflightwith a 60-second lease. - Schedule the next one. In durable mode, before delivering anything, the dispatcher computes tomorrow’s 09:00, writes it to
cronpad.dband adds it todue. A slow webhook can never delay tomorrow’s run. - Deliver. A worker creates the run record (fast mode can start while its commit is pending; see Delivery guarantees), sends the signed
POST, and renews the lease while waiting. - Record. The response is classified as success, retryable or fatal. History commits before job counters, and both commit before acknowledging the occurrence. The attempt is stored; on a retryable failure, a retry entry is added to
duein the same atomic step that removes the in-flight entry. - Measure. Start lag (03:30:00.006 − 03:30:00.000 = 6 ms), duration and end-to-end time go into metrics and the run record. If start lag exceeds the SLA threshold, the run is flagged and an alert can fire.
Source of truth and index
cronpad.dbandhistory.dbare the source of truth. Jobs, their next fire time, users and history live there. Litestream backs them up continuously.- The queue file is an index. It holds only “what runs when” and “what is running now”. It is never backed up: if it is lost, deleted or corrupt, cronpad rebuilds it from
cronpad.dbautomatically. See Scheduling engine.
Read Storage for why SQLite was chosen and its limits.
One binary, one entry point
There is no separate frontend to deploy. API-only mode is a setting ([ui] dashboard = false), not a different build. See Editions for how the open-source and cloud editions differ.