Products / SaaS / Backend Infrastructure

Live

CronLock

Distributed cron for Node + Postgres — no double-runs across instances

Register cron jobs in code, enqueue due fires with idempotent runKey (jobName@fireTime), and claim runs with FOR UPDATE SKIP LOCKED so only one worker executes each fire.

Value

Problem. Running cron on multiple Node processes double-fires the same schedule. Redis-heavy job systems are overkill when you already run Postgres.

How it helps. Register cron jobs in code, enqueue due fires with idempotent runKey (jobName@fireTime), and claim runs with FOR UPDATE SKIP LOCKED so only one worker executes each fire.

Why buy. A thin distributed-cron spine you can read and own — no Redis required for single-region SaaS.

How it works

flowchart LR
  Registry[Job Registry] -->|cron expr + handler| Worker
  Worker -->|enqueue due fires| DB[(PostgreSQL)]
  Worker -->|FOR UPDATE SKIP LOCKED| DB
  Worker -->|run handler| Handler[Job Handler]
  Handler -->|done / failed| DB
  API[Express API] -->|health / jobs / runs| DB

Mermaid flowchart (render in GitHub / VS Code / mermaid.live).

Use case

Two app instances both run the worker; at :00 only one claims heartbeat@… — lockedBy shows the winner; the other gets zero rows.

What you get

Project structure

cronlock/
  src/
    jobs/         heartbeat, cleanup, index
    worker/       schedule, claim, process
    services/     registry
    routes/       jobs, health
    lib/          cron, runKey, config, prisma
  prisma/ tests/ docker-compose.yml

Quick start

docker compose up -d
cp .env.example .env
npm install && npm run prisma:generate && npm run prisma:migrate
npm run dev
npm run worker

Example

From src/services/registry.ts

registerJob({
  name: "send-digest",
  cronExpr: "0 9 * * 1-5", // 09:00 UTC weekdays
  handler: async ({ jobName, scheduledAt, runId }) => {
    // your work — email digests, rollups, etc.
  },
});
// runKey = `${jobName}@${scheduledAt.toISOString()}`

Tested

25 Vitest tests — cron parsing, runKey uniqueness, claim SKIP LOCKED behavior (mocked), process done/failed, registry.

npm test

2026-09-20 — 25 passed; npm run build OK

Design decisions

Limitations

Who it’s for

Teams with multiple Node instances that need scheduled jobs without double-runs.

Not for: Shops that already standardized on Redis queues or need multi-region leader election.

What you own

Full MIT-licensed TypeScript source via Gumroad ZIP. Fork it, ship it commercially, keep the license notice. You are responsible for secrets, hardening, and production ops.

Related

HookQueue · TenantScope · IdemKey · RateGuard

Demo

CronLock demo
Live Postgres demo — claimDueRuns via FOR UPDATE SKIP LOCKED; due run → running → done; future run stays pending.
Buy CronLock — $19 All products