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:
{
"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:
| Expression | Runs |
|---|---|
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:
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.