Back to guides

Guides · April 15, 2026

Node.js cron jobs: every way to schedule Node in 2026 (with code)

Four ways to schedule Node.js in production, with code for each. Pick crontab plus node, an in-process scheduler like node-cron or BullMQ, a platform cron on Vercel, Workers, or Lambda, or external HTTP cron, weighing always-on process cost against serverless cold-start tradeoffs.
crontap.com / guides
Four ways to schedule Node.js: crontab + node-cli, in-process schedulers (node-cron, BullMQ repeatable, Agenda), platform cron (Vercel, Cloudflare Workers, Lambda), and external HTTP cron. Code for each, plus the clustering trap most teams hit.

Node.js powers every-minute Slack digests, hourly Stripe webhook reconciliation, nightly database snapshots to S3. Four credible ways to fire them: drop a script in crontab and run node, run an in-process library like node-cron or BullMQ, use a platform scheduler (Vercel Cron, Cloudflare Workers, AWS Lambda + EventBridge), or let an external cron service like Crontap hit an HTTP endpoint on cadence.

Side-by-side walkthrough with code, for Node.js 22 LTS and 24 (current as of 2026). For Linux cron basics, see What is a cron job in Linux?.

Path 1: crontab + node

The classic. A .js or .mjs file on disk, OS cron firing node.

# crontab -e
*/5 * * * * /usr/local/bin/node --enable-source-maps /opt/app/dist/digest.js >> /var/log/digest.log 2>&1

Three traps eat most Node teams:

  1. Wrong node path. Cron's PATH is tiny. Hardcode the absolute path from which node. /usr/bin/node is fine on plain Ubuntu, on most dev boxes it's /home/deploy/.nvm/versions/node/v22.11.0/bin/node.
  2. nvm not in cron's PATH. The #1 Node cron failure. nvm only loads in interactive shells, so node: command not found shows up even though the binary is there. Symlink to /usr/local/bin/node or source nvm in a wrapper script.
  3. ES modules vs CommonJS. Use .mjs (or "type": "module" in package.json) for top-level await. Pass --enable-source-maps so stack traces map back to your TypeScript.

Pick this when: one VM, one team, SSH access, stable Node version.

Path 2: In-process schedulers

These run inside a long-lived Node process. The tradeoff is always-on cost. You pay for a container or VM 24/7 even when nothing is firing.

  1. node-cron. The simple one. Cron syntax, in-memory, no persistence. Great for a single instance, dies with the process.
  2. cron (the npm package). More featureful, per-job timezones, start/stop control. Still in-memory.
  3. BullMQ repeatable jobs. Redis-backed and durable. Retries, backoff, dead-letter, distributed locking for free. The right answer once you have multiple workers.
  4. Agenda. MongoDB-backed, similar shape to BullMQ for shops already on Mongo.
import cron from "node-cron";
cron.schedule("*/5 * * * *", async () => {
  await sendSlackDigest();
});

Pick this when: you already run an always-on worker and want scheduling colocated. Avoid when: the process can die or scale to zero, or you run more than one replica (see clustering trap below).

Path 3: Platform cron

Cloud platforms ship cron-shaped triggers:

  1. Vercel Cron. Schedules in vercel.json. Hobby is hourly floor, Pro is minute floor. See Vercel cron alternatives and why teams move off Vercel Cron.
  2. Cloudflare Workers Cron Triggers. Edge cron, up to 3 triggers per Worker. See Cloudflare Workers cron alternatives and cron jobs for Cloudflare Workers.
  3. AWS Lambda + EventBridge. Most powerful, most IAM-heavy. See cron jobs for AWS Lambda.
  4. PM2 cron restart. Self-hosting? --cron-restart "0 4 * * *" cycles a worker nightly, a poor man's scheduler for long-running tasks.

Pick this when: the workload already lives on that platform. Watch for: cold starts, timezone quirks, 10-second to 5-minute execution caps.

Path 4: External HTTP cron

Schedule your Node jobs from outside the box. Free forever tier with one schedule. Try Crontap →

Deploy an Express, Fastify, or Hono endpoint behind a bearer header, then point an external cron service at it.

import express from "express";
const app = express();
 
app.post("/internal/slack-digest", async (req, res) => {
  if (req.headers.authorization !== `Bearer ${process.env.CRON_SECRET}`) {
    return res.sendStatus(401);
  }
  await sendSlackDigest();
  res.sendStatus(204);
});

In Crontap: POST https://api.yourapp.com/internal/slack-digest, header Authorization: Bearer <secret>, cron */5 * * * *, timezone America/New_York. Pro is $3.25/mo annual flat for unlimited HTTP schedules at minute cadence on a 1-minute floor.

The clustering trap

The bug that pushes most Node teams to external cron. Run any in-process scheduler (node-cron, cron, naive setInterval) and cluster the app, with PM2 cluster mode, two Kubernetes replicas, multi-region Fly deploys, every replica fires every job. Your every-5-minutes Slack digest goes out 4 times. Stripe reconciliation double-charges.

Two fixes:

  1. Pick-one-leader. Wrap the scheduler in a Redis lock with redlock. Only the leader runs the work. Fragile under network partitions, fine for low-stakes jobs.
  2. Move the schedule layer out of the app. BullMQ (one queue, N workers) or external HTTP cron (one fire, N replicas behind a load balancer).

Once you have more than one replica, in-process scheduling isn't free.

FAQ

Should I use node-cron or BullMQ?

node-cron for one process, no persistence. BullMQ the moment you need retries, multiple workers, or a job that survives a restart.

Why does my cron fire 5 times in production?

You clustered. See above. Fix with a Redis leader lock or move the schedule out of the app.

What about Deno or Bun?

Deno ships Deno.cron baked in (durable on Deno Deploy). Bun runs setInterval and works with node-cron. The four paths above apply, just swap the runtime.

How do I keep the schedule running when my container scales to zero?

You can't. Pin one worker to a minimum instance of 1, or use platform cron or external HTTP cron, so the schedule lives outside the container.

Related on Crontap

From the blog

Read the blog

Guides, patterns and product updates.

Tutorials on scheduling API calls, webhooks and automations, plus deep dives into cron syntax, timezones and reliability.

Alternatives

Vercel Cron every minute: beating the Hobby hourly limit

Vercel Cron caps Hobby at hourly cadence and 5 jobs, and ties every change to a redeploy. Here is the external cron pattern teams use to ship per-minute schedules, per-IANA timezones, and one dashboard across projects without paying $20/mo per user for Pro.

Alternatives

Cloud Run cron without Cloud Scheduler

Cloud Scheduler costs $0.10 per job per month after the first 3 and asks for OIDC plus IAM bindings on every target. Here is the IAM-free pattern Cloud Run teams use to fire their .run.app URLs on a clock with one bearer token and one dashboard across every GCP project.