Virke Cron

Run scheduled tasks at the edge with cron expressions. Virke cron triggers HTTP endpoints in your compute service on a schedule, with distributed locking to ensure at-most-once execution.

Requires a compute project (Virke Run).

Quick Start

1. Add a cron job

virke cron add cleanup "*/5 * * * *" /cron/cleanup

This creates a job named "cleanup" that runs every 5 minutes, calling the /cron/cleanup endpoint in your compute service.

2. Implement the handler

import { Hono } from "hono";
import { virke } from "@virke/runtime/hono";

const { fire, middleware, Bindings } = virke({ db: "my-db" });
const app = new Hono<{ Bindings: typeof Bindings }>();

app.use("*", middleware);

app.post("/cron/cleanup", async (c) => {
  // Verify the request is from Virke cron
  if (c.req.header("x-virke-cron") !== "true") {
    return c.text("Unauthorized", 401);
  }

  // Your cleanup logic
  const result = await c.env.db.execute(
    "DELETE FROM sessions WHERE expires_at < ?",
    [Date.now()]
  );

  return c.json({ success: true, deleted: result });
});

fire(app);

3. Deploy and verify

virke deploy
virke cron list

Cron Expression Format

Standard 5-field cron expressions:

┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6) (Sunday = 0)
│ │ │ │ │
* * * * *

Common schedules

Expression Description
*/5 * * * * Every 5 minutes
0 * * * * Every hour at minute 0
0 0 * * * Every day at midnight
0 0 * * 0 Every Sunday at midnight
0 9 * * 1-5 Every weekday at 9am
*/15 9-17 * * 1-5 Every 15 min, 9am–5pm, Mon–Fri

CLI Commands

Add a job

virke cron add <name> <schedule> <handler>
virke cron add daily-backup "0 0 * * *" /cron/backup

List jobs

virke cron list
Name           Schedule      Handler          Enabled  Last Run             Next Run
daily-backup   0 0 * * *     /cron/backup     Yes      2026-03-04 00:00:00  2026-03-05 00:00:00
cleanup        */5 * * * *   /cron/cleanup    Yes      2026-03-04 17:55:00  2026-03-04 18:00:00

Execution history

virke cron history <name>
Status   Started At           Duration (ms)  Error
Success  2026-03-04 17:55:00  123.45
Success  2026-03-04 17:50:00  98.21
Error    2026-03-04 17:45:00  45.67         Timeout exceeded

Enable / disable

virke cron disable cleanup    # pause without deleting
virke cron enable cleanup     # resume

Remove

virke cron remove cleanup

How It Works

Virke cron uses a "cron-on-traffic" pattern:

  1. On every incoming request to the Virke API, the system checks if any cron jobs are due
  2. If a job is due, a distributed lock is acquired via KV Store
  3. The system makes an HTTP POST to the handler URL in your compute service
  4. Execution results are recorded in the control plane database

At-most-once guarantee

Distributed locks ensure each job executes at most once per schedule interval, even across multiple API instances.

Request headers

Your cron handler receives these headers:

Header Value
x-virke-cron true
x-virke-project-id Your project ID

Configuration

virke.toml

[project]
name = "my-app"

[compute]
entry = "src/worker.ts"
language = "javascript"

[[cron]]
name = "cleanup"
schedule = "*/5 * * * *"
handler = "/cron/cleanup"

[[cron]]
name = "daily-report"
schedule = "0 9 * * 1-5"
handler = "/cron/report"

Best Practices

Verify cron requests

Always check the x-virke-cron header to prevent unauthorized calls:

if (c.req.header("x-virke-cron") !== "true") {
  return c.text("Unauthorized", 401);
}

Keep handlers fast

Cron handlers should complete quickly. For long-running tasks, enqueue work:

app.post("/cron/process", async (c) => {
  await c.env.kv.set("job_queue_process", String(Date.now()));
  return c.json({ queued: true });
});

Return 2xx on success

Non-2xx responses are logged as errors in virke cron history.

Use idempotent operations

At-most-once is guaranteed but not exactly-once, so make handlers idempotent when possible:

// Idempotent — safe to run multiple times
await env.db.execute("DELETE FROM sessions WHERE expires_at < ?", [cutoff]);

// Not idempotent — increments every time
await env.db.execute("UPDATE counters SET value = value + 1");

Limitations

  • Minimum interval: 1 minute
  • Traffic-dependent: Jobs execute only when the API receives traffic. For guaranteed execution with zero traffic, use external health checks.
  • No sub-second precision: Execution times are approximate
  • Single handler per job: One HTTP endpoint per cron job

Further Reading