Background jobs and cron: running work when nobody is online

What background jobs and cron schedules are, how to read a cron expression, how Vercel, GitHub Actions and Supabase run scheduled work, and how to write jobs that survive retries, overlaps and time zones.

Floot ·

Key Takeaways

  • Background jobs move slow work out of the request. Sending email, generating a PDF or calling an AI model can take seconds. Put it on a queue, answer the user immediately, and let a worker do it.
  • Cron runs work on a clock. A cron expression has five fields: minute, hour, day of month, month and day of week. 0 8 * * 1-5 means 08:00 on weekdays.
  • Schedulers deliver late, skip, or repeat. GitHub Actions says scheduled runs can be delayed or dropped under load; Vercel says it can’t rule out duplicate deliveries and doesn’t retry failed function runs. Design for all three.
  • Make every job safe to run twice. Record what each run did, use unique keys for side effects like emails, and check “already done?” before acting.
  • Time zones are part of the schedule. Many schedulers default to UTC. Daylight saving can skip or repeat an hour. For “8am local time” per user, run hourly and select users whose local time is 8.

Quick-Start Prioritization Framework

NeedToolExample
Do slow work after a clickBackground job / queueSend the receipt email after checkout
Repeat on a fixed clockCron scheduleNightly cleanup at 03:00
Run once at a future timeDelayed job or one-time scheduleReminder 24 hours before a booking
React to another serviceWebhook, then a jobProcess a Stripe invoice.paid event
Process a big backlogQueue with batchesRe-index 50,000 records 500 at a time

Some of an app’s most useful work happens when nobody has it open: the 7am digest email, the reminder the day before an appointment, the nightly cleanup of expired invites, the retry of a webhook that failed at 3am. A web request can’t do that work, because there is no request. This guide explains the two tools for it, background jobs and scheduled (cron) jobs, how to read cron syntax, what popular platforms actually guarantee, and the design rules that keep scheduled work from silently skipping or running twice.

Background jobs versus cron jobs

They are often lumped together, but they answer different questions.

Background jobs: “do this soon, not now”

A user action creates a job and puts it on a queue. A worker picks it up and runs it, retrying if it fails. The user gets an instant response. Typical jobs: send an email, resize an upload, call an AI model, sync to a CRM.

Cron jobs: “do this every time the clock says so”

A scheduler triggers code at times defined by an expression, with no user involved. Typical jobs: daily digests, nightly reports, expiring trials, checking for overdue invoices.

Delayed jobs: “do this at that time”

A third pattern sits between them: a job scheduled for a specific future moment, like a reminder 24 hours before an appointment. Some platforms support it directly; Vercel’s newer Schedules feature, for example, offers one-time schedules. Otherwise you can store the due time in a table and have a frequent cron job pick up anything due.

Reading a cron expression

The standard crontab format has five time fields separated by spaces: minute (0-59), hour (0-23), day of month (1-31), month (1-12) and day of week (0-7, where both 0 and 7 mean Sunday). An asterisk means “every”. Commas make lists, hyphens make ranges, and a slash makes steps.

ExpressionMeaning
*/15 * * * *Every 15 minutes
0 * * * *At the start of every hour
0 3 * * *Every day at 03:00
30 7 * * 1-5Weekdays at 07:30
0 9 1 * *09:00 on the 1st of each month
0 18 * * 0Sundays at 18:00

The day-field quirk

In classic cron, if you restrict both day of month and day of week, a job runs when either one matches, not both. 0 9 1 * 1 runs on the 1st of the month and on every Monday. Vercel avoids the confusion by not allowing both to be set at once.

Time zones and daylight saving

The crontab manual notes that during daylight saving changes, times skipped by the clock never match, so those jobs don’t run, and times that occur twice match twice, so jobs run twice. Vercel Cron Jobs and GitHub Actions schedules run in UTC; Vercel’s newer Schedules accept an IANA time zone such as America/New_York and follow daylight saving.

What popular schedulers actually guarantee

A cron expression is a request, not a promise. Here is what three common options document, as of November 2026.

Vercel Cron Jobs and Schedules

Vercel Cron Jobs trigger a function by making an HTTP GET to a path on your production deployment, in UTC; Vercel now calls them a legacy feature to be migrated to Schedules. Schedules add time zones and jitter: on Hobby the jitter window is always 15 minutes, so Hobby schedules run at most every 15 minutes; Pro and Enterprise can set 1 to 15 minutes. Vercel says it delivers each run once in normal operation but can’t rule out duplicates, doesn’t retry a function or path target that errors or times out, and recommends a queue target if a failed run must be retried. Runs are independent, so a slow run can overlap the next.

GitHub Actions schedule

GitHub’s docs say the shortest interval is once every 5 minutes, schedules run on the default branch in UTC, runs can be delayed during high load (the start of every hour is a busy time), and some queued jobs may be dropped. In public repositories, scheduled workflows are disabled after 60 days with no repository activity. It is handy for maintenance scripts, less so for anything customers rely on.

Supabase Cron

Supabase Cron runs on the Postgres pg_cron extension. Jobs can run SQL or database functions inside the database, or make HTTP requests such as calling an Edge Function, on schedules from every second to once a year. Supabase recommends no more than 8 jobs running at once and each job taking no more than 10 minutes.

Your own server

Classic cron on a Linux server is reliable as long as the server is up. If it is down at 03:00, the 03:00 run simply doesn’t happen; nothing catches it up unless you build that.

Five rules for jobs that don’t break

The scheduler will eventually run your job late, twice, or not at all. These rules make that harmless.

1. Make it idempotent

Running the job twice should have the same effect as running it once. For a daily digest, write a row to sent_digests(user_id, digest_date) with a unique constraint before sending; if the insert fails, skip. Vercel’s docs give each run an execution ID for exactly this purpose.

2. Work in small batches

Serverless functions have time limits, and Supabase suggests jobs finish within 10 minutes. Process 200 records, save progress, and let the next run or a queued follow-up continue. A job that tries to do everything in one run fails halfway on the day your data grows.

3. Prevent overlaps

If a job takes longer than its interval, two copies run at once. Take a lock at the start, for example a row in a job_locks table or a Postgres advisory lock, and exit if another run holds it.

4. Select by state, not by time window

“Send reminders for bookings between 24 and 25 hours from now” misses everything if one hourly run is skipped. “Send reminders for bookings in the next 24 hours where reminder_sent_at is null” catches up on the next run and never sends twice.

5. Record and alert

Write a row for each run with its start, end, item count and error. Alert when the last successful run of a daily job is more than 26 hours old. Silent failure is the normal failure mode for scheduled work.

Worked example: a daily 8am digest in every user’s time zone

Users in New York, London and Tokyo should each get their digest at 08:00 local time. A single cron at 0 8 * * * in UTC would reach them at 03:00, 08:00 and 17:00 in winter. Here is a design that works.

  • Store each user’s IANA time zone, such as Europe/London, at sign-up.
  • Schedule one job hourly: 0 * * * *.
  • Each run selects users whose current local hour is 8 and who have no row in sent_digests for their current local date.
  • For each batch of 200, insert the sent_digests row and enqueue the email in the same transaction, then let the email worker send it.
  • If a run is skipped or late, the next hour still finds anyone whose local hour is 8 and hasn’t been sent today. If a run is duplicated, the unique row prevents a second email.
  • Daylight saving is handled because the local hour is computed from the time zone database at run time, not hard-coded as a UTC offset.

Background queues in practice

A queue doesn’t have to be a separate product on day one.

A table is a fine first queue

A jobs table with columns for type, payload, status, attempts, run_after and last_error, plus a worker that claims due rows, covers most small apps. Postgres’s FOR UPDATE SKIP LOCKED lets several workers claim different rows without stepping on each other.

Retries with backoff

When a job fails, increase attempts, set run_after a little later each time (1 minute, 5, 30), and give up after a fixed number, marking it failed so a person can look.

When to graduate

Move to a managed queue when volume or fan-out outgrows a table, or when you need guaranteed retries that your scheduler doesn’t offer. Vercel, for example, points failed-run retries to its queue targets.

Common mistakes

These cause most missed and duplicated scheduled work.

Assuming the scheduler is in your time zone

Many default to UTC. Check before you promise customers “9am”.

Leaving the cron endpoint public

If a scheduled job is triggered by calling a URL, anyone who finds the URL can trigger it. Require a secret header; Vercel, for instance, sends a CRON_SECRET for this.

Testing only by waiting

Make every job runnable on demand with a manual trigger, and test it against copies of real data, including the day daylight saving changes.

Free-tier hosting that sleeps

Some free plans pause or spin down idle projects. A scheduled job can’t run on a backend that is asleep, so check your host’s inactivity rules before relying on it.

If you’d rather not run a scheduler yourself

If you’d rather not set up and monitor schedulers and queues yourself, Floot apps include scheduled jobs and background tasks that run with nobody online, and the agent you build with can add them alongside the app’s database and endpoints.

Frequently Asked Questions

What is a cron job in simple terms?

A task a computer runs automatically on a repeating schedule, such as every night at 3am. The name comes from cron, the Unix program that runs scheduled commands.

Can a serverless app run cron jobs?

Yes. Most platforms trigger a function on a schedule, for example Vercel Cron Jobs or Schedules, or pg_cron in Supabase. The function needs to finish within the platform’s time limit, so large jobs should work in batches.

How often can GitHub Actions run a scheduled workflow?

At most once every 5 minutes, in UTC, on the default branch. GitHub notes runs can be delayed or dropped under heavy load.

Why did my cron job run twice?

Common causes are a duplicate delivery by the scheduler, an overlapping slow run, a daylight saving change that repeats an hour, or two deployments both carrying the schedule. Idempotent jobs make all of these harmless.

What is the difference between a queue and a cron job?

A queue runs work when it is added, usually after a user action. A cron job runs work when the clock matches. Many apps use a cron job to find due items and put them on a queue.

How do I send something at a specific time for each user?

Store the due time or the user’s time zone, run a frequent job (every minute to every hour), and select items that are due and not yet done.

Sources