npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@prsm/cron

v1.0.0

Published

Redis-backed distributed cron scheduler with leader election and cron expression support

Readme

Redis-backed distributed cron scheduler. Run jobs on a schedule across multiple instances - only one fires per tick.

Installation

npm install @prsm/cron

Quick Start

import { Cron } from '@prsm/cron'

const cron = new Cron()

cron.add('cleanup', '*/5 * * * *', async () => {
  return await db.query('DELETE FROM temp WHERE created_at < NOW() - INTERVAL 1 HOUR')
})

cron.on('fire', ({ name, result }) => {
  console.log('Fired:', name, result)
})

cron.on('error', ({ name, error }) => {
  console.error('Failed:', name, error.message)
})

await cron.start()

Schedule Formats

Three ways to define when a job runs:

cron.add('reports', '0 2 * * *', handler) // cron expression
cron.add('heartbeat', '30s', handler) // duration string
cron.add('poll', 5000, handler) // milliseconds

Cron Expressions

Standard 5-field format: minute hour day-of-month month day-of-week

| Field | Range | Allowed | | ------------ | ----- | ----------------------------------- | | Minute | 0-59 | *, ,, -, / | | Hour | 0-23 | *, ,, -, / | | Day of month | 1-31 | *, ,, -, / | | Month | 1-12 | *, ,, -, /, names (jan-dec) | | Day of week | 0-7 | *, ,, -, /, names (sun-sat) |

Day 0 and 7 both mean Sunday. When both day-of-month and day-of-week are specified (not *), either matching triggers the job (OR logic, per standard cron).

Shortcuts

| Shortcut | Equivalent | | ----------- | ----------- | | @yearly | 0 0 1 1 * | | @annually | 0 0 1 1 * | | @monthly | 0 0 1 * * | | @weekly | 0 0 * * 0 | | @daily | 0 0 * * * | | @midnight | 0 0 * * * | | @hourly | 0 * * * * |

Duration Strings

Parsed by @prsm/ms: '100ms', '5s', '1m', '1h'.

Options

const cron = new Cron({
  redis: {
    host: 'localhost',
    port: 6379,
    password: 'secret',
  },
  prefix: 'myapp:cron:', // default: 'cron:'
})

Exclusive Mode

By default, if a handler runs longer than the interval, the next tick can start a new execution on another instance. Enable exclusive mode to prevent overlapping:

cron.add(
  'reports',
  {
    schedule: '0 2 * * *',
    exclusive: true,
    exclusiveTtl: '30m', // max lock hold time (default 10m)
  },
  async () => {
    await generateDailyReport()
  }
)

While one instance is running the handler, all other instances (and subsequent ticks on the same instance) skip until it completes. The TTL is a safety net - if the instance crashes, the lock auto-releases after exclusiveTtl.

Job Management

cron.add('a', '30s', handler) // register
cron.add('b', '1m', handler) // chainable - returns this
cron.remove('a') // stop and unregister
cron.jobs // ['b']
cron.nextFireTime('b') // Date or null

Jobs can be added before or after start(). Adding after start begins scheduling immediately.

Events

cron.on('fire', ({ name, tickId, result }) => {})
cron.on('error', ({ name, tickId, error }) => {})

How It Works

Each instance runs its own timers. When a timer fires, it attempts a Redis SET key NX PX ttl for that specific tick window. Only one instance succeeds - the rest see the key already exists and skip. No leader election protocol, no consensus - just an atomic Redis operation.

For interval jobs, ticks are epoch-aligned: tickId = Math.floor(Date.now() / interval). All instances compute the same tick ID independently, so they compete for the same lock regardless of when they started.

For cron jobs, ticks are minute-aligned: tickId = Math.floor(Date.now() / 60000). The cron parser computes the next matching minute and sets a timeout for it.

Scheduled Queue Processing with queue

Push work into a queue on a schedule:

import { Cron } from '@prsm/cron'
import Queue from '@prsm/queue'

const cron = new Cron()
const queue = new Queue({ concurrency: 5 })

queue.process(async (payload) => {
  return await syncTenant(payload)
})

cron.add('sync-all-tenants', '0 */6 * * *', async () => {
  const tenants = await db.query('SELECT id FROM tenants WHERE active = true')
  for (const t of tenants) {
    await queue.group(t.id).push({ tenantId: t.id })
  }
})

await queue.ready()
await cron.start()

Every 6 hours, one instance enqueues sync tasks for all tenants. The queue distributes the actual work across all instances with per-tenant concurrency control.

Real-Time Status with mesh

Broadcast scheduled job results to connected clients:

import { Cron } from '@prsm/cron'
import { MeshServer } from '@mesh-kit/server'

const mesh = new MeshServer({ redis: { host: 'localhost', port: 6379 } })
const cron = new Cron()

cron.add('leaderboard', '*/5 * * * *', async () => {
  return await computeLeaderboard()
})

cron.on('fire', ({ name, result }) => {
  mesh.broadcastRoom('dashboard', `cron:${name}`, result)
})

await mesh.listen(8080)
await cron.start()

Cleanup

await cron.stop()

Clears all timers, waits for in-flight handlers to complete, then disconnects Redis.

Horizontal Scaling

All lock state lives in Redis. Deploy as many instances as you want - Redis SET NX guarantees exactly-once execution per tick. No configuration changes needed. Lock keys auto-expire, so crashed instances don't leave stale locks.

License

MIT