# Delivery guarantees

cronpad guarantees **at-least-once** delivery: every scheduled run is attempted, and in rare failure cases it may be delivered twice. It never silently drops a run while its database is intact. Every run records how late it started. The Cloud Pro on-time objective is 99.9% within **5 seconds** per calendar month, with the exclusions defined below. Each job can also set its own SLA threshold (5 seconds by default) to flag and alert on individual late runs.

## At least once

A run is delivered more than once only in these situations:

| Situation | Why it duplicates |
|---|---|
| cronpad stops (crash, `kill -9`, power loss) after your endpoint received the request but before cronpad recorded the result | cronpad can't know the request arrived, so the next process delivers it again. |
| The queue file can't be written for longer than the lease (60 s) while a request is in flight (for example, a full disk) | The lease can't be renewed; the work is considered abandoned and is redelivered. |
| A retry after a timeout | Your endpoint may have done the work but responded too slowly. The retry is a new attempt of the same run. |

### Making your endpoint idempotent

Every request for a run carries the same identifier on every attempt and every redelivery:

| Header | Value | Stable across |
|---|---|---|
| `Idempotency-Key` | Run ID, e.g. `run_01J9ZM1A…` | Retries and redeliveries |
| `webhook-id` | Same run ID (Standard Webhooks) | Retries and redeliveries |
| `Cronpad-Attempt` | `0`, `1`, `2`… | Changes per attempt |

Store the run ID when you finish the work, and skip work you've already done for that ID. If your endpoint already supports `Idempotency-Key` (many payment and email APIs do), point cronpad at it directly.

## Durable and fast delivery

`scheduler.delivery_mode = "durable"` is the default in both self-hosted and hosted mode. The run is recorded before the delivery starts, and the result is recorded before cronpad acknowledges it.

Opt-in `"fast"` starts the first attempt as soon as the run is queued for recording, so delivery overlaps the database commits. If cronpad crashes in that short window, the receiver may already have acted on a request that was never recorded; after recovery the run is delivered again with the same `Idempotency-Key` and `webhook-id`. If the job was deleted in between, recovery records the run as `cancelled` with the reason `job deleted before recovery; delivery may have occurred` and does not redeliver. In-flight work survives the crash in the queue file, so recovery finds it on the next start.

**Receivers must deduplicate with `Idempotency-Key` or `webhook-id` before you enable fast mode.** In fast mode the recorded start time of a replayed run is the replay's, since the original delivery's timing and response are unknown.

Runs that are subject to a monthly quota, a concurrency cap or `skip_if_running` always wait for admission, even in fast mode, so limits and overlap policy hold. A slow endpoint never blocks the schedule. Shutdown drains in-flight runs and flushes pending records in both modes.

## Never silently dropped

- Each active job's next fire time is stored in `cronpad.db` before cronpad relies on the queue for it.
- If the queue entry for a job disappears for any reason, the reconciler notices that the job is overdue and re-queues it. If the whole queue file is deleted, emptied or replaced, cronpad detects the missing epoch and rebuilds the queue. Pending and in-progress runs are stored durably before the schedule advances, so a rebuild also restores work whose lease disappeared; a run invalidated by a job edit is not revived.
- Runs missed while cronpad was down are handled by the job's [misfire policy](../schedules/misfires-and-overlap.md): fire once now (default), fire each, or skip. Skipped runs are still recorded with status `skipped`, so the gap is visible.

## On time

Each run records `start_lag_ms`: the time between `scheduled_at` and the start of its first attempt.

- A run **breaches its SLA** when `start_lag_ms` is greater than the job's SLA threshold (job setting `sla_threshold`, or the server default of `5s`).
- Breaches are recorded on the run (`sla_breached: true`), counted in the `cronpad.runs.sla_breached` metric, and can trigger a `run.sla_breached` [alert](../alerts/index.md).
- Retries don't count against start lag; they're measured by `e2e_ms`.

Start lag includes timer slack and the durable database commits that record the start (a single job typically starts 2–4 ms after its scheduled time). Lag grows when more work is due at once than there are workers (for example thousands of jobs at exactly midnight); cronpad still runs everything, and the metrics show the lag honestly rather than hiding it.

### Cloud SLO and SLA

A **service-level objective (SLO)** is a published performance target, without service credits. A **service-level agreement (SLA)** is a contractual commitment with defined service credits. [Contact us](mailto:contact@cronpad.com) for availability and terms.

- **Cloud Free:** best effort.
- **Cloud Pro:** at least **99.9%** of scheduled runs per UTC calendar month start on time, excluding announced maintenance windows and runs delayed by the workspace's own concurrency cap. There are no service credits.
- **Cloud Enterprise:** a contractual on-time SLA with service credits on **dedicated single-tenant instances**. The exact target and credit terms are agreed with the customer.
- No credit-bearing SLA is offered on shared infrastructure.

**On time** means `start_lag ≤ 5s` for the first attempt. The start is the outbound request being sent or the command starting; endpoint response time is excluded. Retries are measured separately with `e2e_ms`. A job's `sla_threshold` remains its individual alert threshold, not a credit-bearing contract. Self-hosted timing depends on your hardware and configuration; benchmark figures are not Cloud commitments.

### Maintenance

Announced maintenance windows are excluded from the monthly SLO calculation. Affected runs still fire late and are never silently lost. Deploys can pause scheduling for a few seconds.

### Visible admission exceptions

Licensing, plan downgrades and billing problems never stop the scheduler. An exceeded Free-plan quota (or an optional Pro spend cap) records new occurrences as `skipped` with `quota_exceeded`; an operator suspension for abuse records skipped runs with `workspace_suspended`. These apply only to admission; runs that were admitted are delivered normally. Retries never consume monthly quota.

## Ordering

- No ordering is guaranteed between different jobs that are due at the same moment.
- For one job, the next occurrence fires on schedule even if the previous run is still retrying, unless you set `overlap_policy` to `skip_if_running`. See [Misfires and overlap](../schedules/misfires-and-overlap.md).

## What cronpad does not guarantee

- **Exactly-once.** No scheduler can guarantee it over HTTP; use the idempotency key.
- **Delivery to an endpoint that is down for longer than your retry policy allows.** After the last attempt the run is `failed`, you get an alert if you configured one, and after enough consecutive failures the job is [auto-disabled](../retries-and-failures/auto-disable.md).
- **Uptime during a self-hosted deploy.** A restart pauses scheduling for a few seconds; runs due in that window start late (and are marked as SLA breaches) but are not lost.