Configuration

Crons

Call a route of your production deployment on a schedule.

Declare crons

Add them to si.json in the Serverless App Service's root directory and push to the production branch:

si.json
{
  "crons": [{ "path": "/api/cron/cleanup", "schedule": "cron(0 3 * * ? *)" }]
}

When the deployment becomes production, each entry becomes a schedule. Crons need a server, so they're for Next.js Serverless App Services.

Schedule expressions

Schedules use EventBridge Scheduler expressions, in UTC:

ExpressionRuns
rate(5 minutes)Every 5 minutes
rate(1 hour)Every hour
rate(1 day)Every day
cron(0 3 * * ? *)Every day at 03:00
cron(0/15 * * * ? *)Every 15 minutes, on the quarter hour
cron(0 12 ? * MON-FRI *)Weekdays at 12:00
cron(0 9 1 * ? *)09:00 on the first of each month

cron(…) takes six fields: minutes, hours, day of month, month, day of week and year. One of day of month and day of week must be ?. rate(…) takes a number and minute(s), hour(s) or day(s).

A run can start up to 5 minutes after its scheduled time.

The request

At each run, the platform calls your current production deployment at its own deployment hostname:

POST /api/cron/cleanup HTTP/1.1
Host: <project>-<id>-<org>.cactivecloud.com
x-si-cron: <SI_CRON_SECRET>
user-agent: si-cron
  • There's no body.
  • The platform waits up to 120 seconds, but the server function stops at the Serverless App Service's max duration (30 seconds unless changed in Settings → Functions).
  • A response of 500 or above is retried up to 2 more times. Any other status counts as done.
  • Nothing is called while the Serverless App Service has no production deployment.

Check the secret

The route is public like the rest of your deployment, so check the x-si-cron header against SI_CRON_SECRET, which the platform sets on every server function:

app/api/cron/cleanup/route.ts
import { timingSafeEqual } from "node:crypto"

function fromCron(request: Request) {
  const sent = Buffer.from(request.headers.get("x-si-cron") ?? "")
  const secret = Buffer.from(process.env.SI_CRON_SECRET ?? "")
  return secret.length > 0 && sent.length === secret.length && timingSafeEqual(sent, secret)
}

export async function POST(request: Request) {
  if (!fromCron(request)) return new Response("Unauthorized", { status: 401 })
  // … the scheduled work
  return Response.json({ ok: true })
}

The secret belongs to the Serverless App Service and stays the same across deployments.

Crons follow production

Every deployment records its crons. When a deployment becomes production (a production push, a promote or a rollback), its crons replace the Serverless App Service's schedules, so schedules always match the code that's serving. Preview deployments never receive cron requests.

Pause, next run and last result

The Serverless App Service's Settings → Cron Jobs lists the production deployment's crons with each one's next run (UTC) and the result of its last call: the HTTP status and duration, or why there was no response.

Pause stops every cron of the Serverless App Service (needs projects:write); Resume starts them again. A paused Serverless App Service stays paused through new deployments, promotions and rollbacks until you resume it.