Cron - live.cron()

Run scheduled tasks on the server.

// src/live/cleanup.js
import { live } from 'svelte-realtime/server';

export const cleanup = live.cron('*/5 * * * *', async (ctx) => {
  const deleted = await db.sessions.deleteExpired();
  console.log(`Cleaned up ${deleted} expired sessions`);
});

Schedule format

Standard cron syntax: minute hour day month weekday. A leading 6th field unlocks sub-minute granularity: second minute hour day month weekday.

ExpressionMeaning
* * * * *Every minute
*/5 * * * *Every 5 minutes
0 * * * *Every hour
0 0 * * *Daily at midnight
0 0 * * 1Every Monday at midnight
*/10 * * * * *Every 10 seconds (6-field)
0 */30 * * * *Every 30 seconds (6-field)

Once any 6-field schedule is registered, the cron tick adapts from once-per-minute to 1 Hz (sticky for the process lifetime). 5-field schedules running under the 1 Hz tick fire only at second :00 of any matching minute, preserving once-per-matching-minute semantics.

live.cron does not fire concurrently with itself: the tick skips a path whose previous invocation is still in flight and increments cronCount{status: 'skipped'}.

Auto-publish to a topic

Pass a topic name as the second argument. The cron function publishes its return value as a set event on that topic:

export const refreshStats = live.cron('*/5 * * * *', 'stats', async () => {
  return { users: await db.users.count(), orders: await db.orders.todayCount() };
});

Pair it with a merge: 'set' stream:

export const stats = live.stream('stats', async (ctx) => {
  return db.stats();
}, { merge: 'set' });

Publishing from cron with ctx.publish

The function receives a ctx object with publish, publishThrottled, publishDebounced, and signal - the same helpers available in RPC handlers (minus user and ws, since cron runs outside a connection; also no ctx.skip, which is per-call and meaningless on a fixed schedule). Use ctx.publish for fine-grained control, e.g. publishing individual created/deleted events on a crud stream:

export const cleanup = live.cron('0 * * * *', 'boards', async (ctx) => {
  const stale = await listStaleBoards();
  for (const board of stale) {
    await deleteBoard(board.board_id);
    ctx.publish('boards', 'deleted', { board_id: board.board_id });
  }
  // returning undefined skips the automatic 'set' publish
});

If the function returns a value, it is published as a set event. If it returns undefined, no automatic publish happens - this lets you use ctx.publish exclusively without an unwanted set event overwriting your crud updates.

Platform capture

The platform is captured automatically from the first RPC call. If your app starts cron jobs before any WebSocket connections, call setCronPlatform(platform) from the init hook (recommended) or the open hook (fallback):

// hooks.ws.js - recommended call site (fires once per worker, before any open)
import { setCronPlatform, message, close, unsubscribe } from 'svelte-realtime/server';

export function init({ platform }) {
  setCronPlatform(platform);
}

export { message, close, unsubscribe };

The init lifecycle hook eliminates the boot-to-first-connect window where cron ticks were no-ops. The legacy open(ws, platform) call site continues to work as a fallback. See Lifecycle Hooks.

Cluster mode

In a clustered deployment (SO_REUSEPORT, acceptor cluster, multiple replicas), every worker would tick the same cron expression independently and fire the handler N times. Pick one of two wiring styles:

Added in 0.5.6. One declaration of cluster intent reaches every framework publish surface (RPC, cron, reactive seam, top-level publish()) in lockstep:

// src/hooks.ws.js
import { realtime } from 'svelte-realtime/server';
import { createRedis, createPubSubBus, createLeader } from 'svelte-adapter-uws-extensions/redis';

const redis = createRedis();
const bus = createPubSubBus(redis);
const leader = createLeader(redis);

export const { open, close, message, init } = realtime({
  bus,
  leader: leader.isLeader
});

export function upgrade({ cookies }) {
  return validateSession(cookies.session_id) || false;
}

Single-replica is the same file with realtime() (no config).

Layer-1: manual configureCron

For fine-grained control (per-route bus, conditional cluster wiring), configureCron is still the first-class primitive:

import { configureCron } from 'svelte-realtime/server';
import { createLeader } from 'svelte-adapter-uws-extensions/redis/leader';
import { createPubSubBus } from 'svelte-adapter-uws-extensions/redis/pubsub';

export async function init({ platform }) {
  const leader = await createLeader(redis, { key: 'cron-leader' });
  const bus = await createPubSubBus(redis);

  configureCron({
    leader: () => leader.isLeader(),
    bus
  });
}
FieldDescription
leader: () => booleanSync predicate. If false, the worker skips the tick. Throwing leader is treated as fail-closed (skip tick, increment cron{status: 'leader-error'}).
busOptional. A pubsub bus consumed structurally as { wrap(platform): wrapped }. As of 0.5.6, passing bus to configureCron writes the same process-wide bus state as setBus(bus) so the reactive seam (live.effect, live.derived, live.aggregate, live.webhook) also picks up cluster relay.

configureCron(null) clears any previously installed config. At least one of leader or bus must be present; passing neither throws. A leader set without bus produces a warn-once diagnostic - you almost always want both.

See createLeader for the Redis-backed leader-election primitive, and Distributed Pub/Sub for the bus.

Use cases

  • Clean up expired data
  • Poll external APIs and push results
  • Aggregate metrics
  • Send periodic heartbeats

Was this page helpful?