Cron Scheduling

Schedule recurring tasks with Elysia's cron plugin

Cron Scheduling

Eden Stack supports scheduled tasks using the @elysiajs/cron plugin, powered by Croner. Cron jobs run in-process alongside your Elysia API server.

When to Use Cron vs Inngest

FeatureElysia CronInngest Scheduled
PersistenceIn-memory onlyDurable, survives restarts
Retry on failureNo built-in retryAutomatic retries
MonitoringManual loggingDashboard + history
Setup complexityMinimalRequires Inngest account
Best forDev tools, health checks, lightweight tasksCritical business logic, billing, reports

Rule of thumb: Use Elysia cron for lightweight, non-critical tasks. Use Inngest cron for anything that must not be missed.

Setup

Install the plugin in your API app:

bun add @elysiajs/cron --filter=@eden/api

Basic Usage

import { cron, Patterns } from "@elysiajs/cron";
import { Elysia } from "elysia";
 
const app = new Elysia()
  .use(
    cron({
      name: "heartbeat",
      pattern: Patterns.EVERY_MINUTE,
      run() {
        console.log("Server is alive", new Date().toISOString());
      },
    })
  )
  .listen(3001);

Cron Expression Syntax

┌────────────── second (optional)
│ ┌──────────── minute
│ │ ┌────────── hour
│ │ │ ┌──────── day of month
│ │ │ │ ┌────── month
│ │ │ │ │ ┌──── day of week
│ │ │ │ │ │
* * * * * *

Common expressions:

ExpressionSchedule
0 */5 * * *Every 5 minutes
0 0 9 * * *Daily at 9:00 AM
0 0 0 * * 1Every Monday at midnight
0 0 1 1 *Yearly on Jan 1st

Predefined Patterns

The plugin ships with readable pattern helpers:

import { Patterns } from "@elysiajs/cron";
 
Patterns.EVERY_MINUTE
Patterns.EVERY_HOUR
Patterns.EVERY_DAY_AT_MIDNIGHT
Patterns.EVERY_WEEK
Patterns.EVERY_WEEKDAY
 
// Dynamic patterns
Patterns.everyMinutes(5)
Patterns.everyDayAt("09:00")
Patterns.everyWeekOn(Patterns.MONDAY, "09:00")
Patterns.everyWeekdayAt("17:00")

Configuration Options

cron({
  name: "my-job",               // Required — registered in store.cron
  pattern: "0 9 * * *",         // Required — cron expression
  timezone: "America/New_York", // Optional — defaults to system
  catch: true,                  // Optional — continue on error
  maxRuns: 100,                 // Optional — stop after N executions
  startAt: new Date("2025-06-01"), // Optional — delay start
  stopAt: new Date("2025-12-31"), // Optional — auto-stop
  run() {
    // Your job logic
  },
});

Controlling Jobs at Runtime

Jobs are accessible through store.cron:

const app = new Elysia()
  .use(
    cron({
      name: "reports",
      pattern: Patterns.everyDayAt("09:00"),
      run() { generateReport(); },
    })
  )
  .get("/cron/stop/:name", ({ store, params }) => {
    store.cron[params.name]?.stop();
    return { stopped: params.name };
  })
  .get("/cron/trigger/:name", ({ store, params }) => {
    store.cron[params.name]?.trigger();
    return { triggered: params.name };
  });

Available methods on each job:

  • .stop() — Pause the job
  • .resume() — Resume a paused job
  • .trigger() — Run immediately
  • .nextRun() — Next scheduled execution time
  • .previousRun() — Last execution time

SaaS Use Cases

Here are practical examples of cron jobs you might add to your SaaS:

Trial Expiration Reminders

cron({
  name: "trial-expiration-check",
  pattern: Patterns.everyDayAt("08:00"),
  timezone: "UTC",
  catch: true,
  async run() {
    const expiringSoon = await db
      .select()
      .from(users)
      .where(
        and(
          eq(users.subscriptionStatus, "trialing"),
          lte(users.trialEndsAt, addDays(new Date(), 3))
        )
      );
 
    for (const user of expiringSoon) {
      await sendEmail({
        to: user.email,
        subject: "Your trial expires soon",
        template: TrialExpiringEmail({ name: user.name }),
      });
    }
  },
});

Stale Data Cleanup

cron({
  name: "cleanup-expired-sessions",
  pattern: Patterns.EVERY_HOUR,
  catch: true,
  async run() {
    const deleted = await db
      .delete(sessions)
      .where(lt(sessions.expiresAt, new Date()));
    console.log(`[CRON] Cleaned up ${deleted.rowCount} expired sessions`);
  },
});

Usage Metrics Aggregation

cron({
  name: "aggregate-daily-metrics",
  pattern: Patterns.everyDayAt("00:05"),
  timezone: "UTC",
  catch: true,
  async run() {
    const yesterday = subDays(new Date(), 1);
    const metrics = await db
      .select({
        workspaceId: apiCalls.workspaceId,
        count: count(),
      })
      .from(apiCalls)
      .where(gte(apiCalls.createdAt, yesterday))
      .groupBy(apiCalls.workspaceId);
 
    await db.insert(dailyUsage).values(
      metrics.map((m) => ({
        workspaceId: m.workspaceId,
        apiCallCount: m.count,
        date: yesterday,
      }))
    );
  },
});

Weekly Digest Email

cron({
  name: "weekly-digest",
  pattern: Patterns.everyWeekOn(Patterns.MONDAY, "09:00"),
  timezone: "UTC",
  catch: true,
  async run() {
    const activeUsers = await db
      .select()
      .from(users)
      .where(eq(users.digestEnabled, true));
 
    for (const user of activeUsers) {
      const stats = await getWeeklyStats(user.id);
      await sendEmail({
        to: user.email,
        subject: "Your Weekly Summary",
        template: WeeklyDigestEmail({ name: user.name, stats }),
      });
    }
  },
});

Health Check Ping

cron({
  name: "external-health-check",
  pattern: Patterns.everyMinutes(5),
  catch: true,
  async run() {
    const services = ["https://api.stripe.com", "https://api.resend.com"];
    for (const url of services) {
      try {
        const res = await fetch(url, { method: "HEAD" });
        if (!res.ok) console.warn(`[HEALTH] ${url} returned ${res.status}`);
      } catch (err) {
        console.error(`[HEALTH] ${url} unreachable:`, err);
      }
    }
  },
});

Organizing Cron Jobs

Keep cron jobs in a dedicated route file:

apps/api/src/
├── routes/
│   ├── cron.ts          # All cron job definitions
│   ├── payments.ts
│   └── ...
└── index.ts             # .use(cronRoutes)

For many jobs, split into separate files:

apps/api/src/
├── cron/
│   ├── index.ts         # Combines all cron plugins
│   ├── cleanup.ts       # Data cleanup jobs
│   ├── notifications.ts # Email/push notification jobs
│   └── metrics.ts       # Analytics aggregation jobs

Demo Job

Eden Stack includes a demo cron job at apps/api/src/routes/cron.ts that sends a weekly email every Monday at 09:00 UTC. It uses the CronDemoEmail template from @eden/email.

Check cron status:

curl http://localhost:3001/api/cron/status

Set CRON_DEMO_RECIPIENT in your .env to receive the demo email, or remove the job once confirmed.

Next Steps

Full documentation for Eden Stack users

This documentation is exclusively available to Eden Stack customers. Already purchased? Log in to access the full content.