Migrate from crontab and other schedulers
Delay runs scheduled work by calling a URL in your app. Moving over means each task gets an HTTP endpoint, each schedule becomes a Delay cron in UTC, and Delay’s run history and alerts replace your log files and failure emails.
What changes
| In Delay | |
|---|---|
| What runs | An HTTP request (GET, POST, PUT, PATCH or DELETE) to a public URL. A shell command needs an endpoint in your app. |
| Time zone | UTC, for every cron. There is no per-cron time zone and no daylight saving shift. |
| Syntax | Standard 5 fields, one-minute precision. No seconds or year field, no @reboot. @hourly, @daily, @weekly, @monthly and @yearly work, and so do names like MON-FRI. |
| Failed run | Not retried. The cron waits for its next scheduled time. |
| Overlap | Each run starts on time, even if the previous one is still going. |
| Time limit | Delay waits up to timeout_seconds (default 30, at most 300) for a response. |
| Success | A 2xx response, plus any success assertions you add. Redirects are not followed, so a 3xx is a failure. |
| Reachability | Delay calls from the internet. localhost, private networks and other internal addresses are blocked when the request runs. |
| Output | Each run’s status code, headers and body are kept in the run history. |
If a task needs retries, have the cron’s endpoint hand the work to a job with POST /api/jobs: jobs are retried on their queue’s schedule.
Example: one crontab line to a Delay cron
Here is a crontab entry on a server set to New York time:
30 2 * * 1-5 cd /var/www/app && php artisan reports:send >> /var/log/reports.log 2>&1| crontab | Delay |
|---|---|
30 2 * * 1-5 (02:30 New York time, weekdays) | "cron_expression": "30 7 * * 1-5" (07:30 UTC) |
php artisan reports:send | "url": "https://api.yourapp.com/tasks/send-reports" with "method": "POST" |
>> /var/log/reports.log | The cron’s Execution history |
MAILTO emails | "alert_threshold": 1 and a Slack or Discord channel |
First, add a route to your app that runs the same code as reports:send and returns a 2xx. If the work takes more than a few seconds, queue it and return 202 straight away (see endpoint design). Then create the cron, paused, so it doesn’t run until you cut over:
curl -X POST https://app.delay.run/api/crons \ -H "Authorization: Bearer $DELAY_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "name": "Send reports (weekdays 07:30 UTC)", "url": "https://api.yourapp.com/tasks/send-reports", "method": "POST", "cron_expression": "30 7 * * 1-5", "timeout_seconds": 60, "alert_threshold": 1, "is_active": false }'A paused cron comes back with "next_run_at": null. When you’re ready, remove the crontab line and click Start on the cron’s page in Delay, in that order and close together, so the task neither runs twice nor skips a day.
Move from crontab
- List your entries with
crontab -l, and check the system crontabs (/etc/crontaband/etc/cron.d/) for others. - For each command, add an HTTP endpoint in your app that runs the same code. Protect it: verify Delay’s signature, or check a secret header you set in the cron’s
headers. - Convert each schedule from your server’s time zone to UTC.
- Replace
@rebootentries with something in your app’s start-up. Delay has no equivalent. - If a task uses
flockor a lock file to avoid overlapping runs, keep that lock in the endpoint. - Create each cron with
"is_active": false. - Cut over one task at a time: remove the crontab line, click Start on the cron, and watch its first runs under Recent executions.
Move from cron-job.org or EasyCron
These services already call URLs, so you copy each job’s settings across.
| What you set in the old tool | Delay field |
|---|---|
| URL | url |
| HTTP method | method, in capitals. Crons default to GET. |
| Schedule | cron_expression, in UTC. Convert it if the old job used a local time zone. |
| Request headers | headers, a JSON object |
| Request body | payload, as a string. JSON in the string is sent as a JSON body. |
| Timeout | timeout_seconds, 1 to 300 |
| Failure notifications | alert_threshold (1, 2 or 3 failures in a row) and a Slack or Discord channel |
| Expected status code or text in the response | A success assertion, added in the dashboard after you create the cron |
For example, a job that POSTs a small JSON body every 6 hours with an API key header:
curl -X POST https://app.delay.run/api/crons \ -H "Authorization: Bearer $DELAY_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "name": "Sync inventory", "url": "https://api.yourapp.com/sync/inventory", "method": "POST", "headers": {"X-Api-Key": "your-endpoint-key"}, "payload": "{\"full\": false}", "cron_expression": "0 */6 * * *", "timeout_seconds": 120, "alert_threshold": 2 }'If the old job’s URL redirects (for example from http:// to https://), use the final URL. Delay treats a redirect as a failure.
Move from a cron monitor
If you run cron jobs that ping a monitor such as Cronitor or Healthchecks.io after each run, you don’t need the ping in Delay. Delay sends the request itself, so it records every run and knows when one fails. Delay has no inbound ping URL.
| Monitor setup | In Delay |
|---|---|
| Ping at the end of the job | Not needed. Each run is recorded with its status, duration and response. |
| Alert when a ping is late or missing | A run that times out, can’t connect or gets a non-2xx response is a failure. Set alert_threshold to alert after 1, 2 or 3 in a row. |
| Maximum run time | timeout_seconds, or a Response time under (ms) assertion |
| Check the job’s output | A Body contains or JSON path equals assertion |
| Recovery notice | Sent when a cron that triggered an alert succeeds again |
| Slack, Discord or email notifications | Email, Slack or Discord, set in Settings > Notifications. With Email, cron failure and recovery alerts go to every team member. |
| Send results to another system | callback_url: Delay POSTs the outcome after every run. See callbacks. |
Move from platform cron (Vercel, Netlify, Render, Fly.io)
Platform schedulers call a route or run a command inside your app. To move a task, make sure it is reachable as a normal HTTPS route, create a Delay cron for its full URL, then remove the platform schedule.
| Platform | What to do |
|---|---|
| Vercel Cron Jobs | Vercel calls the path in vercel.json with GET, which is also the Delay cron default. Create a cron for the full production URL, then remove the entry from vercel.json. Vercel schedules are in UTC too, so the expression carries over. |
| Netlify scheduled functions | Remove the function’s schedule setting so it becomes a regular function, and point the Delay cron at that function’s URL. |
| Render cron jobs | These run a command in a separate service. Add a route to your web service that runs the same code. |
| Fly.io (cron in a Machine, or scheduled Machines) | Add a route to your app that runs the same code. |
If your Vercel route checks CRON_SECRET, add the same header to the cron’s JSON so the route doesn’t change:
{ "headers": {"Authorization": "Bearer your-cron-secret"}}Your platform’s own time limit for a request still applies, and Delay waits at most 300 seconds.
Before you switch off the old scheduler
- Your API token is saved as a secret where you run the API calls (see quick start).
- Cron alerts go to Slack or Discord: set it in Settings > Notifications (set up alerts).
- Optional: a team admin generates a signing secret under Settings > Webhook signing, and your endpoints verify it.
- Your monthly request quota covers the new crons. Every run counts: a cron every minute is 44,640 requests in a 31-day month. See runs and usage.
Next steps
- Cron schedules: the syntax Delay accepts, UTC and timing.
- Examples: ready-made crons for backups, reports, health checks and more.
- Schedule a recurring request: edit, pause and run crons in the dashboard.
- Endpoint design: how Delay treats your endpoint’s response.