Skip to content

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 runsAn HTTP request (GET, POST, PUT, PATCH or DELETE) to a public URL. A shell command needs an endpoint in your app.
Time zoneUTC, for every cron. There is no per-cron time zone and no daylight saving shift.
SyntaxStandard 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 runNot retried. The cron waits for its next scheduled time.
OverlapEach run starts on time, even if the previous one is still going.
Time limitDelay waits up to timeout_seconds (default 30, at most 300) for a response.
SuccessA 2xx response, plus any success assertions you add. Redirects are not followed, so a 3xx is a failure.
ReachabilityDelay calls from the internet. localhost, private networks and other internal addresses are blocked when the request runs.
OutputEach 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
crontabDelay
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.logThe 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:

Terminal window
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

  1. List your entries with crontab -l, and check the system crontabs (/etc/crontab and /etc/cron.d/) for others.
  2. 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.
  3. Convert each schedule from your server’s time zone to UTC.
  4. Replace @reboot entries with something in your app’s start-up. Delay has no equivalent.
  5. If a task uses flock or a lock file to avoid overlapping runs, keep that lock in the endpoint.
  6. Create each cron with "is_active": false.
  7. 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 toolDelay field
URLurl
HTTP methodmethod, in capitals. Crons default to GET.
Schedulecron_expression, in UTC. Convert it if the old job used a local time zone.
Request headersheaders, a JSON object
Request bodypayload, as a string. JSON in the string is sent as a JSON body.
Timeouttimeout_seconds, 1 to 300
Failure notificationsalert_threshold (1, 2 or 3 failures in a row) and a Slack or Discord channel
Expected status code or text in the responseA 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:

Terminal window
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 setupIn Delay
Ping at the end of the jobNot needed. Each run is recorded with its status, duration and response.
Alert when a ping is late or missingA 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 timetimeout_seconds, or a Response time under (ms) assertion
Check the job’s outputA Body contains or JSON path equals assertion
Recovery noticeSent when a cron that triggered an alert succeeds again
Slack, Discord or email notificationsEmail, Slack or Discord, set in Settings > Notifications. With Email, cron failure and recovery alerts go to every team member.
Send results to another systemcallback_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.

PlatformWhat to do
Vercel Cron JobsVercel 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 functionsRemove the function’s schedule setting so it becomes a regular function, and point the Delay cron at that function’s URL.
Render cron jobsThese 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