JoSk
"JoSk" is a Node.js task manager for horizontally scaled apps: clusters, multi-server setups, and multi-threaded instances on one or many machines or data centers. It works the same in a single-instance app.
"JoSk" mimics the native setTimeout and setInterval API and supports CRON expressions. Tasks sync between all app instances through Redis, MongoDB, PostgreSQL, or a custom adapter. Storage-level leases and atomic claims give each due tick to one instance; delivery guarantees depend on the scheduling method.
Note: JoSk is the server-only package.
ToC
- Main features
- Prerequisites
- Install as NPM package
- API
- Execution semantics
- TypeScript
- Examples
- Prefix mapping
- Operational FAQ
- Migration guides
- Notes
- Running tests
- Why "JoSk"
- Support
Main features
- 🏢 Synchronize single task across multiple servers;
- 🔏 Read locking to avoid simultaneous task executions across complex infrastructure;
- 📦 Zero dependencies, written from scratch for top performance;
- 👨🔬 ~99% tests coverage;
- 💪 Bulletproof design, built-in retries, and "zombie" task recovery 🧟🔫.
Prerequisites
node@>=20.9.0redis-server@>=5.0.0or a single-writer KeyDB/Valkey server forRedisAdapter, withredis@^4orredis@^5. CI targets selected standalone images; see Redis Adapter for exact coverage.mongod@>=4.4forMongoAdapter, with the officialmongodbNPM package (only the official driver is tested)postgres@>=12forPostgresAdapter, withpg@>=8.0.3(pg@7does not connect on Node 14+)bun@>=1.1.0(optional), see Bun runtime
Older releases compatibility
node@<20.9.0— usejosk@^5mongod@<4.0.0— usejosk@=1.1.0node@<14.20.0— usejosk@=3.0.2node@<8.9.0— usejosk@=1.1.0
Install:
npm install josk --save
1// ES Module Style 2import { JoSk, RedisAdapter, MongoAdapter, PostgresAdapter } from 'josk'; 3 4// CommonJS 5const { JoSk, RedisAdapter, MongoAdapter, PostgresAdapter } = require('josk');
Bun runtime
Since v6.0.0
JoSk runs unmodified on Bun >=1.1.0 with the same mongodb, pg, and redis drivers. Install with bun add josk. Node and Bun processes can share one prefix, since leases and claims live in storage. npm run test:bun runs the Jest suite under bun:test, see Running tests.
Agent Skill
JoSk ships an Agent Skill with the public API, adapter setup, execution semantics, CRON and handler patterns, and common pitfalls. It works with 50+ AI coding agents; pass -a claude-code to target one.
# Install the JoSk skill globally: npx skills add veliovgroup/josk -g # Or install the JoSk skill into the current project: npx skills add veliovgroup/josk
The skill is distributed via GitHub and is not part of the npm tarball.
API:
new JoSk(opts)
opts.adapter{RedisAdapter|MongoAdapter|PostgresAdapter} - [Required] Instance of adapter or customopts.debug{Boolean} - [Optional] Enable debugging messages, useful during developmentopts.autoClear{Boolean} - [Optional] Remove stored tasks that have no handler registered in this instance. Such tasks appear after a task is renamed or removed from code, or when instances with different codebases share one storage. Default:falseopts.zombieTime{Number} - [Optional] Interval recovery hold in milliseconds, used when a handler never callsready()or its process dies. Caught handler errors go toonErrorand complete the run; they do not wait for zombie recovery. Keep it above the slowest legitimate handler runtime plus margin; below60000is not recommended. Default:900000(15 minutes)opts.lockLeaseTime{Number} - [Optional] Scheduler lease TTL in milliseconds. Default:min(zombieTime, 30000), floored at2 * maxRevolvingDelay + 1000. See v6.3 migration notesopts.execute{String} - [Optional]batchdrains all due tasks under one scheduler lease;oneclaims one task per lease. Default:batchopts.concurrency{Number} - [Optional] Maximum handlers running in parallel in this instance. Use a positive integer when handlers share rate-limited resources;Infinitydisables the cap. Default:Infinityopts.lockOwnerId{String} - [Optional] Stable owner id used as the prefix of scheduler lease tokens, for observability and for re-claiming this instance's leases after a planned restart. Default:crypto.randomUUID()per instanceopts.minRevolvingDelay{Number} - [Optional] Minimum delay between scheduler polls in milliseconds. Default:128opts.maxRevolvingDelay{Number} - [Optional] Maximum delay between scheduler polls in milliseconds. Default:768opts.onError{Function} - [Optional] Informational hook, called instead of throwing exceptions. Default:false. Called with two arguments:title{String}details{Object}details.description{String}details.error{Mix}details.uid{String} - Internaluid, suitable for.clearInterval()and.clearTimeout()details.task{Mix} - Present only for malformed-task errors; the offending task payload
opts.onExecuted{Function} - [Optional] Informational hook, called when a handler signals completion, including after a caught error. It does not indicate success. Default:false. Called with two arguments:uid{String} -uidpassed into.setImmediate(),.setTimeout(), orsetInterval()methodsdetails{Object}details.uid{String} - Internaluid, suitable for.clearInterval()and.clearTimeout()details.date{Date} - Execution timestamp as JS {Date}details.delay{Number} - Executiondelay(e.g.intervalfor.setInterval())details.timestamp{Number} - Execution timestamp as unix {Number}
Hook throws and async rejections are logged and isolated from scheduler execution.
new RedisAdapter(opts)
Since v5.0.0
opts.client{RedisClient} - [Required] Connected client, e.g. fromawait createClient().connect()orcreateCluster()opts.prefix{String} - [Optional] use to create multiple named instancesopts.resetOnInit{Boolean} - [Optional] (use with caution) Delete all tasks for thisprefixon init. For single-instance apps that need a clean start after a crash. Default:falseopts.useHashTags{Boolean} - [Optional] Use hash-tag keys (josk:{prefix}:*) so all adapter keys live in one slot. Required for Redis Cluster; the constructor throws for a cluster client without it. Default:false(josk:prefix:*)
new MongoAdapter(opts)
Since v5.0.0
opts.db{Db} - [Required]Dbinstance fromMongoClient#db()opts.prefix{String} - [Optional] use to create multiple named instancesopts.lockCollectionName{String} - [Optional] Lock collection name. Default:__JobTasks__.lock, shared by all JoSk instancesopts.resetOnInit{Boolean} - [Optional] (use with caution) Delete all tasks for thisprefixon init. For single-instance apps that need a clean start after a crash. Default:false
new PostgresAdapter(opts)
Since v6.0.0
opts.client{Pool|Client} - [Required]pgclient with.query()method. UsePoolfor long-running appsopts.prefix{String} - [Optional] Isolated scheduler namespace in the same database. Default:defaultopts.resetOnInit{Boolean} - [Optional] (use with caution) Delete tasks and locks for thisprefixon init. Default:false
Initialization
JoSk has no dependencies. Install the driver for your adapter: redis, mongodb, or pg. To use other storage, write a custom adapter.
Redis Adapter
RedisAdapter keeps due timestamps in a sorted set and task payloads in a hash, and claims due tasks with Lua scripts. It uses the Redis-compatible commands shared by Redis, KeyDB, and Valkey; CI targets Redis 6/7/8, eqalpha/keydb:x86_64_v6.3.4, and valkey/valkey:8.1.9-alpine in standalone mode using redis@5.
- Use one writable primary. Do not route JoSk reads or writes to replicas; claims must be visible to all instances at once.
- For Redis Cluster, pass
useHashTags: true; CI tests a 3-master Redis Cluster. KeyDB/Valkey Cluster modes are not separately tested. - Avoid KeyDB active-replication (multi-master). Its conflict resolution can let two writers claim the same task.
- For strict single-claim scheduling across data centers, use a strongly consistent store, or PostgreSQL with one write authority.
1import { JoSk, RedisAdapter } from 'josk'; 2import { createClient } from 'redis'; 3 4const redisClient = await createClient({ 5 url: 'redis://127.0.0.1:6379' 6}).connect(); 7 8const jobs = new JoSk({ 9 adapter: new RedisAdapter({ 10 client: redisClient, 11 prefix: 'app-scheduler', 12 // useHashTags: true, // Redis Cluster; KeyDB/Valkey Cluster not CI-tested 13 }), 14 onError(reason, details) { 15 // Catches exceptions thrown inside scheduled tasks 16 console.log(reason, details.error); 17 } 18});
MongoDB Adapter
MongoAdapter creates two collections per prefix: one for tasks and one with the .lock suffix for scheduler locks.
1import { JoSk, MongoAdapter } from 'josk'; 2import { MongoClient } from 'mongodb'; 3 4const client = new MongoClient('mongodb://127.0.0.1:27017'); 5// Use a separate DB from the "main" one to avoid lock contention 6const mongoDb = client.db('joskdb'); 7const jobs = new JoSk({ 8 adapter: new MongoAdapter({ 9 db: mongoDb, 10 prefix: 'cluster-scheduler', 11 }), 12 onError(reason, details) { 13 // Catches exceptions thrown inside scheduled tasks 14 console.log(reason, details.error); 15 } 16});
PostgreSQL Adapter
Since v6.0.0
PostgresAdapter creates and migrates the josk_tasks and josk_locks tables on init, in the client's current database and schema. Table names are fixed; prefix isolates namespaces.
- Use
pg.Pool. Share the app's pool, or give the scheduler a small dedicated one. - Use one writable primary. Do not route JoSk reads or writes to replicas.
- Prefer a dedicated database or schema.
- Keep
resetOnInit: falsein clustered production. execute: 'batch'claims due tasks withFOR UPDATE SKIP LOCKED;execute: 'one'usesLIMIT 1per lease.
1import { JoSk, PostgresAdapter } from 'josk'; 2import { Pool } from 'pg'; 3 4const pool = new Pool({ 5 connectionString: 'postgres://user:pass@localhost:5432/joskdb' 6}); 7 8const jobs = new JoSk({ 9 adapter: new PostgresAdapter({ 10 client: pool, 11 prefix: 'cluster-scheduler', 12 }), 13 onError(reason, details) { 14 // Catches exceptions thrown inside scheduled tasks 15 console.log(reason, details.error); 16 } 17});
Create the first task
1jobs.setInterval((ready) => { 2 /* ...code here... */ 3 ready(); 4}, 60 * 60000, 'task1h'); // every hour 5 6// No need to call ready() when the handler takes no arguments and returns a Promise 7jobs.setInterval(async () => { 8 await asyncMethod(); 9}, 30 * 60000, 'asyncTask30m'); // every 30 mins
uid identifies a task across the cluster. Use a different uid for each schedule, even for the same function:
1jobs.setInterval(task, 60000, 'task-1m'); // every minute 2jobs.setInterval(task, 2 * 60000, 'task-2m'); // every two minutes
setInterval(func, delay, uid)
func{Function} - Function to call on schedule. Receivesreadyas the first argumentdelay{Number} - Delay for the first run of a new task and interval between further executions in millisecondsuid{String} - Unique app-wide task id- Returns: {
Promise<string>}
The next run is scheduled when the handler calls ready(), so a run never overlaps the previous one:
1jobs.setInterval((ready) => { 2 /* ...code... */ 3 ready(); 4}, 60 * 60000, 'task1h'); // every hour + handler runtime 5 6jobs.setInterval((ready) => { 7 ready(); // schedule the next run first 8 /* ...code... */ 9}, 60 * 60000, 'task1hStrict'); // every hour, the next run may start before this one ends
With callback APIs, call ready() on every path, including errors:
1jobs.setInterval((ready) => { 2 asyncCall((error, result) => { 3 if (error) { 4 ready(); // <-- Always call `ready()`, even if the call failed 5 return; 6 } 7 8 waitForSomethingElse(result, () => { 9 ready(); // <-- End of the full execution 10 }); 11 }); 12}, 0, 'longRunningTask'); // runs again as soon as the previous run is finished
When the app restarts and calls setInterval() again for a stored task, JoSk picks the next run from the stored state. A task is claimed while an instance runs it and has not yet called ready():
| Stored task | Next run |
|---|---|
None, or not claimed with a different delay | now + delay |
Not claimed, same delay | Earlier of the stored executeAt and now + delay. A past-due task runs on the first poll after boot |
| Claimed | Unchanged. It stays held until ready() or zombieTime expires, even if delay changed |
To restart an unclaimed interval's countdown, call clearInterval() before registering it again. setTimeout() and setImmediate() always schedule from registration time.
ready() argument forms
ready returns Promise<boolean> and accepts:
ready()— schedule the next interval run atnow + delay.ready(date)—Date; schedule the next interval run at that moment. A date in the past falls back tonow + delay. Use with CRON.ready(timestamp)— ms-epoch number; same asready(date).ready(callback)— Node-style(error, success) => void. It fires before the storage update; await the returned Promise to wait for persistence.
Dates and timestamps only apply to setInterval. setTimeout and setImmediate tasks are removed from storage before their handler runs.
Calling ready() twice rejects the returned Promise (or passes an error to the callback) with "Resolution method is overspecified". Call it at most once per run.
When a handler takes no arguments (async () => { … } or () => doSomething()), JoSk calls ready() when its returned Promise settles.
setTimeout(func, delay, uid)
func{Function} - Function to call afterdelay. Receivesreadyas the first argumentdelay{Number} - Delay in millisecondsuid{String} - Unique app-wide task id- Returns: {
Promise<string>}
Runs at most once across the cluster. Use it where a duplicate run is worse than a missed one.
1jobs.setTimeout((ready) => { 2 asyncCall(() => { 3 /* ...code... */ 4 ready(); 5 }); 6}, 60000, 'taskIn1m'); 7 8jobs.setTimeout(async () => { 9 await asyncMethod(); 10}, 60000, 'asyncTaskIn1m');
setImmediate(func, uid)
func{Function} - Function to execute. Receivesreadyas the first argumentuid{String} - Unique app-wide task id- Returns: {
Promise<string>}
Runs once, as soon as the next scheduler poll claims it. At most once across the cluster, like setTimeout.
1jobs.setImmediate((ready) => { 2 /* ...code... */ 3 ready(); 4}, 'syncTask'); 5 6jobs.setImmediate(async () => { 7 await asyncMethod(); 8}, 'asyncTask');
clearInterval(timerId)
timerId{String|Promise<string>} — Timer id returned fromJoSk#setInterval()method- Returns: {
Promise<boolean>}truewhen task is successfully cleared, orfalsewhen task was not found
1const timer = await jobs.setInterval(func, 34789, 'unique-taskid'); 2await jobs.clearInterval(timer);
clearTimeout(timerId)
timerId{String|Promise<string>} — Timer id returned fromJoSk#setTimeout()method- Returns: {
Promise<boolean>}truewhen task is successfully cleared, orfalsewhen task was not found
1const timer = await jobs.setTimeout(func, 34789, 'unique-taskid'); 2await jobs.clearTimeout(timer);
destroy()
- Returns: {boolean}
trueif instance successfully destroyed,falseif instance already destroyed
Stops this instance's scheduler. After destroy(), only clearTimeout() and clearInterval() work; other methods report an error to onError (or stdout).
destroy() does not wait for running handlers. Tasks this instance claimed but had not started go back to storage. An interval killed mid-run is recovered by another instance after zombieTime; use shutdown() before process exit to avoid that wait.
shutdown(opts)
Since v6.4.0
opts.timeout{Number} - [Optional] Milliseconds to wait for running handlers to callready(). Default:10000- Returns: {
Promise<boolean>}trueif every running handler finished withintimeout
Calls destroy(), waits for running handlers, then hands unfinished interval claims back to storage so another instance runs them on its next poll instead of after zombieTime. A handler that calls ready() after its claim was handed back does not change the schedule; onExecuted still fires because the handler ran. Repeated calls share the first shutdown attempt and timeout; when it expires, JoSk reports every unfinished handler to onError (or console.error), hands current interval claims back, and abandons one-shot tasks to preserve at-most-once behavior. The latest superseded handler of each task also counts as unfinished, but its obsolete claim is left unchanged; older superseded handlers are not tracked. Keep at-most-once handlers idempotent, or set timeout longer than the longest one-shot handler; keep it below your platform's termination grace period.
1const shutdown = async () => { 2 await jobs.shutdown({ timeout: 10000 }); 3 process.exit(0); 4}; 5 6process.on('SIGTERM', shutdown); 7process.on('SIGINT', shutdown);
ping()
- Returns: {
Promise<object>}
Checks scheduler readiness and the storage connection.
1const pingResult = await jobs.ping(); 2// OK: { status: 'OK', code: 200, statusCode: 200 } 3// Failed: { status: 'Error reason', code: 500, statusCode: 500, error: ErrorObject }
pause()
- Signature:
pause()orpause(timerId) - Returns: {boolean}
trueif pause state changed,falseif already paused for that scope
For multi-instance setups with long-running handlers only. Other instances keep running tasks while this one is paused. Single-instance apps and short handlers gain nothing.
pause()— this instance stops taking the scheduler lease. Running handlers continue, and tasks stay in storage.pause(timerId)— when this instance claims that task, it reschedules it without running the handler, so another instance can run it. Pass the timer id returned byset*, not the bareuid.
1if (loadSheddingActive()) { 2 jobs.pause(); // stop competing while saturated 3} 4const heavy = await jobs.setInterval(worker, 60_000, 'heavy-sync'); 5jobs.pause(heavy); // only skip this task on this instance
resume()
- Signature:
resume()orresume(timerId) - Returns: {boolean}
trueif pause cleared,falseif not paused
resume() clears the global pause; resume(timerId) clears a per-task pause. The instance competes again on its next poll.
1jobs.resume(); // global 2jobs.resume(heavy); // per timer id from set*
Queue-polling pattern: after winning a tick, claim rows from your own queue, call pause(), call ready(), process the rows, then resume() in finally. JoSk releases the tick quickly and this instance stops competing until local work ends. Full examples: skills/josk/references/patterns.md.
Execution semantics
| Method | Guarantee | Notes |
|---|---|---|
setTimeout, setImmediate | At-most-once across the cluster | Task is removed from storage before the handler runs. If the process dies mid-run, the run is lost. |
setInterval | At-least-once per tick (until cleared) | Task stays in storage while running. If ready() is not called within zombieTime, another instance may claim and run it again. Make handlers idempotent. |
zombieTime (default 15 minutes) is the safety net for stuck handlers. Set it above your slowest legitimate handler plus storage latency. Restarting an app does not shorten a running interval's hold. So after an unclean kill, recovery waits the full zombieTime, not delay, but a rolling deploy cannot start a second copy of a handler that is still running. Call shutdown() on SIGTERM to hand running claims back instead of leaving them to zombieTime. JoSk skips a late ready() write when a newer same-uid run starts in the same process. Built-in adapters also fence writes across instances; custom adapters need equivalent fencing for that guarantee. See adapter-api.md and monitoring and recovery.
execute: 'batch'(default) claims all due tasks under one lease, for throughput.execute: 'one'claims one task per lease, for smaller bursts and fairer spread across instances.concurrencycaps parallel handlers in this instance. DefaultInfinity, like Node's timers. Set a limit when handlers share DB connections or API rate limits.pause()andpause(timerId)are per process. Combine them withconcurrencyandexecute: 'one'when one instance should back off during long local work.
TypeScript
JoSk ships declarations for ESM (index.d.ts) and CommonJS (index.d.cts). They don't import redis or mongodb, so unused drivers are not needed. RedisAdapter and MongoAdapter keep your client's full type on adapter.client / adapter.db. The JoSkAdapter interface is exported for custom adapters.
1import { JoSk, RedisAdapter } from 'josk'; 2import type { JoSkAdapter, JoSkOption, JoSkOnError } from 'josk'; 3import { createClient } from 'redis'; 4 5const onError: JoSkOnError = async (title, details) => { 6 console.error(title, details.error, details.uid); 7}; 8 9const adapter: JoSkAdapter = new RedisAdapter({ 10 client: await createClient({ url: process.env.REDIS_URL }).connect(), 11 prefix: 'cluster-scheduler' 12}); 13 14const options: JoSkOption = { adapter, execute: 'batch', concurrency: 16, onError }; 15const jobs = new JoSk(options);
Examples
CRON
Use cron-parser@^5 to compute each next run and pass it to ready(date):
1import { CronExpressionParser } from 'cron-parser'; 2 3const setCron = (uid, cronExpr, task) => { 4 const nextRun = () => CronExpressionParser.parse(cronExpr).next().toDate(); 5 6 return jobs.setInterval(async (ready) => { 7 try { 8 await task(); 9 } finally { 10 await ready(nextRun()); // schedule the next run even if the task fails 11 } 12 }, Math.max(0, +nextRun() - Date.now()), uid); 13}; 14 15await setCron('daily-report', '0 9 * * *', () => sendReport());
Errors thrown by task still reach onError. For second-level CRON expressions, raise minRevolvingDelay / maxRevolvingDelay (e.g. 512 / 1000) to cut storage reads.
Pass arguments
Wrap the task in a closure:
1const task = (arg1, arg2, ready) => { 2 //... code here 3 ready(); 4}; 5 6jobs.setInterval((ready) => task({ key: 'value' }, 'A', ready), 60 * 60000, 'taskA'); 7jobs.setInterval((ready) => task({ key: 'other' }, 'B', ready), 60 * 60000, 'taskB');
Clean up old tasks
For development and tests.
Clean up Redis
redis-cli --no-auth-warning --scan --pattern "josk:default:*" | xargs redis-cli --raw --no-auth-warning DEL # If you're using multiple JoSk instances with prefix: redis-cli --no-auth-warning --scan --pattern "josk:prefix:*" | xargs redis-cli --raw --no-auth-warning DEL # If useHashTags is true: redis-cli --no-auth-warning --scan --pattern "josk:{prefix}:*" | xargs redis-cli --raw --no-auth-warning DEL
Clean up MongoDB
1// Run directly in MongoDB console (default prefix `default`): 2db.getCollection('__JobTasks__default').deleteMany({}); 3// If you're using a custom prefix: 4db.getCollection('__JobTasks__PrefixHere').deleteMany({}); 5// Lock collection (shared across prefixes by default): 6db.getCollection('__JobTasks__.lock').deleteMany({});
Clean up PostgreSQL
DELETE FROM josk_tasks WHERE prefix = 'default'; DELETE FROM josk_locks WHERE lock_key = 'josk-default.lock'; -- If you're using custom prefix: DELETE FROM josk_tasks WHERE prefix = 'cluster-scheduler'; DELETE FROM josk_locks WHERE lock_key = 'josk-cluster-scheduler.lock';
MongoDB connection fine tuning
Replica-set tuning, dedicated-DB advice, the index list, and Mongo-compatible service notes: docs/mongodb.md. Cosmos DB and DocumentDB aren't part of default CI; optional endpoint tests require configured secrets.
Prefix mapping
prefix isolates scheduler state. Instances with the same prefix share one queue. Default: default.
| Adapter | Storage layout for prefix: 'app' | Notes |
|---|---|---|
| Redis | Default keys: josk:app:schedule, josk:app:tasks, josk:app:lock. With useHashTags: true: josk:{app}:schedule, josk:{app}:tasks, josk:{app}:lock. | Hash tags keep all keys on the same Cluster slot. Prefix must match /^[A-Za-z0-9_\-:.]+/ — special characters (notably { and }) are rejected to protect Cluster routing. |
| MongoDB | Collection __JobTasks__app; lock collection __JobTasks__.lock (shared across prefixes, scoped by uniqueName field) | Override the lock collection with lockCollectionName. Keep collection names short — Mongo's name limit is 120 characters including database name. |
| PostgreSQL | Rows in josk_tasks filtered by prefix='app'; lock row in josk_locks with lock_key='josk-app.lock' | Table names are fixed. Use prefix for tenant/environment isolation. |
Operational FAQ
How do I monitor stuck tasks?
An interval that never calls ready() becomes claimable again after zombieTime. This recovery does not fire onError. The 'One of your tasks is missing' error means this instance claimed a task it has no handler for. Past-due tasks show backlog, not stuck work. To find claims near their recovery deadline, see monitoring.
How do I handle storage restarts?
JoSk catches adapter errors and retries on the next poll. Locks held by crashed nodes expire on their own (Redis: PEXPIRE, Mongo: TTL index, Postgres: locked_until against server time).
one vs batch execute mode?
Use batch for throughput; it claims due tasks in chunks. Use one for smaller bursts per instance, fairer spread across instances, or when handlers contend on the same downstream resource.
Jitter: Why is my interval running every delay + maxRevolvingDelay ms?
JoSk polls every minRevolvingDelay to maxRevolvingDelay ms, so the effective interval is delay plus poll latency. Lower maxRevolvingDelay for tighter intervals at the cost of more storage reads.
What about clock skew between nodes?
Redis uses relative PX TTLs. Postgres computes lease expiry from CURRENT_TIMESTAMP, so node clock skew does not change lock lifetime. Mongo stores app-generated dates; keep Mongo app nodes time-synchronized.
Notes
- Use JoSk when several copies of an app run the same repeating tasks and each due tick must run once cluster-wide, not once per instance. Examples: email and SMS queues, long polling, periodic sync.
- Keep task intervals at 2 seconds or more. Shorter tasks may overlap with the storage round-trip plus poll delay.
- Accuracy is
delay ± (maxRevolvingDelay + storage latency): about ±0.8s plus storage latency with defaults. Tighter bounds cost more storage reads. - Poll delays are random within
minRevolvingDelay..maxRevolvingDelayso instances don't hit storage at the same moment. Defaults (128..768) suit a 3-server setup. A higherminRevolvingDelayreduces storage reads and writes.
Running tests
Setup, full and targeted suites, coverage, and the Bun runner: docs/testing.md. Quickstart:
REDIS_URL="…" MONGO_URL="…" PG_URL="…" npm test
Why JoSk?
JoSk is Job-Task - Is randomly generated name by "uniq" project
Support our open source contribution:
- Try 🌉 Bridge CDN - A SEO-focused alternative to Cloudflare. CDN, DNS, IndexNow, Prerender, SEO, Edge Computing.
- Upload and share files using ☄️ meteor-files.com — Continue interrupted file uploads without losing any progress. There is nothing that will stop Meteor from delivering your file to the desired destination
- Use ▲ ostr.io for Server Monitoring, Web Analytics, WebSec, Web-CRON and SEO Pre-rendering of a website
- Star on GitHub
- Star on NPM
- Star on Atmosphere
- Sponsor via GitHub
- Support via PayPal