DEV Community

delay.run
delay.run

Posted on

How to schedule an HTTP request (call a URL or webhook on a schedule)

A surprising number of background jobs boil down to one thing: call this URL every hour. Refresh a cache, sync orders from an API, send a daily digest, ping a webhook. Your app already has the logic behind an endpoint. You just need something to hit it on schedule.

This guide covers three ways to do that, the curl flags that make the difference between a reliable job and a silent one, and how to protect the endpoint so only your scheduler can trigger it.

Option 1: crontab and curl

If you have a Linux server, this is the quickest route. Run crontab -e and add a line:

*/15 * * * * curl -fsS --max-time 30 https://example.com/tasks/sync >> /var/log/sync.log 2>&1
Enter fullscreen mode Exit fullscreen mode

That calls the URL every 15 minutes. The flags matter more than they look:

  • -f makes curl exit with an error on HTTP 4xx and 5xx responses. Without it, a 500 from your app counts as success.
  • -sS hides the progress bar but still prints errors.
  • --max-time 30 stops a hung request from piling up behind the next run.
  • >> ... 2>&1 keeps a log, so you can see what happened at 3 am.

Sending a POST with JSON and a secret

Most task endpoints should reject anonymous requests. Keep the token in a file readable only by the cron user, not in the crontab itself:

#!/usr/bin/env bash
# /usr/local/bin/call-sync.sh
set -euo pipefail
TOKEN="$(cat /etc/myapp/cron-token)"

curl -fsS --max-time 30 \
  -X POST https://example.com/tasks/sync \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"source":"cron"}'
Enter fullscreen mode Exit fullscreen mode
*/15 * * * * /usr/local/bin/call-sync.sh >> /var/log/sync.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Putting the request in a script also avoids cron's % problem. Inside a crontab, % means newline, so a JSON body or a date +%F in the line breaks in confusing ways unless you escape it as \%.

Retrying transient failures

curl can retry on its own:

curl -fsS --max-time 30 --retry 3 --retry-delay 10 https://example.com/tasks/sync
Enter fullscreen mode Exit fullscreen mode

--retry covers timeouts and responses like 429, 502, 503 and 504. Only use it if the endpoint is safe to call twice.

Option 2: a systemd timer

On systemd-based servers, timers are a more structured alternative to cron. You get logs in the journal and the option to catch up on missed runs.

# /etc/systemd/system/sync.service
[Unit]
Description=Call the sync endpoint

[Service]
Type=oneshot
ExecStart=/usr/local/bin/call-sync.sh
Enter fullscreen mode Exit fullscreen mode
# /etc/systemd/system/sync.timer
[Unit]
Description=Run sync every 15 minutes

[Timer]
OnCalendar=*:0/15
Persistent=true

[Install]
WantedBy=timers.target
Enter fullscreen mode Exit fullscreen mode
sudo systemctl daemon-reload
sudo systemctl enable --now sync.timer
systemctl list-timers sync.timer
journalctl -u sync.service --since today
Enter fullscreen mode Exit fullscreen mode

Persistent=true runs the job once at boot if the machine was off when it was due.

Option 3: a hosted HTTP scheduler

Both options above need a server that's always on, and both fail quietly when that server goes away. If the job is only an HTTP call, a hosted scheduler removes the server from the picture. You give it a URL and a schedule, and it makes the request.

delay.run works this way: one API call creates a recurring cron (standard five-field syntax, as often as every minute) or a one-off job that runs right away or after a delay of up to 24 hours. Every run is logged with the request, response body, status code and latency, and you can get alerts by email, Slack or Discord when a cron fails several times in a row. It's plain HTTP with no SDK, so it works with any language or framework.

The main thing to compare between hosted options is what you can see after the fact: whether you get the response body of a failed run, and whether anything tells you when a schedule keeps failing.

Secure the endpoint

A task URL that anyone can trigger is an invitation for abuse. Pick one of these:

A shared secret in a header. Simple and good enough for most internal jobs:

// Express
app.post('/tasks/sync', (req, res) => {
  if (req.get('authorization') !== `Bearer ${process.env.CRON_TOKEN}`) {
    return res.status(401).end();
  }
  // ... do the work ...
  res.json({ ok: true });
});
Enter fullscreen mode Exit fullscreen mode

A signed request. Better when the scheduler supports it. The sender computes an HMAC of the body and a timestamp with a shared key, and your endpoint recomputes it. The timestamp stops someone replaying an old request:

import crypto from 'node:crypto';

function verify(rawBody, timestamp, signature, secret) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;  // 5 minutes
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');
  return signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Enter fullscreen mode Exit fullscreen mode

The exact header names and the string that gets signed depend on the scheduler, so check its docs. Always verify against the raw request body, before JSON parsing changes it.

Design the endpoint for scheduled calls

A few habits make scheduled HTTP jobs much easier to live with:

  1. Return a real status code. 200 when the work succeeded, 5xx when it didn't. Schedulers and curl -f rely on it.
  2. Respond with a short summary. {"synced": 42} in the response body shows up in logs and makes "did it actually do anything?" easy to answer.
  3. Be idempotent. Retries, overlaps and manual re-runs will happen. Running the same job twice shouldn't send two emails or charge a card twice.
  4. Keep it short or hand off. If the work takes minutes, accept the request, queue the work, and return quickly. Long requests hit timeouts on proxies and serverless platforms.

Quick checklist

  • curl uses -f and --max-time
  • secrets live in a file or environment variable, not in the crontab
  • the endpoint rejects requests without the secret or a valid signature
  • the endpoint returns 5xx on failure
  • there's a log of every run, and something alerts you when runs keep failing

Get those right and "call this URL every 15 minutes" becomes a job you can forget about, in the good way.

Top comments (0)