EMZETT.
Login

Cron Job (Vercel)

In short: A route that Vercel calls automatically by itself at fixed times (instead of a browser or user visiting it) — for recurring background tasks that nobody has to trigger manually.

In more detail: In Vercel, a cron job is declared in vercel.json (path + schedule in cron format, e.g. daily at a specific time). Vercel then sends a request to this route itself — the code behind it is a completely normal API route, technically no different from one that a person calls.

Our context: At Emzett, /api/ticket-reminder uses this for automatic reminder emails when admin messages in support tickets remain unanswered for 12+ hours — it runs daily, without Michael having to think about it himself.

In Depth

Declaration and cron syntax

The declaration in vercel.json follows standard cron syntax (minute, hour, day, month, weekday):

{
  "crons": [
    { "path": "/api/ticket-reminder", "schedule": "0 12 * * *" },
    { "path": "/api/cart-cleanup", "schedule": "0 9 * * *" }
  ]
}

"0 12 * * *" means: every day at 12:00 UTC. The five fields (from left to right: minute 0-59, hour 0-23, day of month 1-31, month 1-12, weekday 0-6) can be combined with * (every value), lists (1,15), ranges (1-5) and step values (*/15 for every 15 minutes) — the same syntax as the classic Unix cron daemon, which Vercel’s implementation deliberately mimics so that existing cron knowledge carries over directly.

Authenticating cron calls

An important security aspect: since the cron route is technically a completely normal, publicly reachable API route, anyone else could in principle call it manually too — Vercel therefore sets a special Authorization header (Bearer <CRON_SECRET>) on real cron calls, which the route should check on the server side to prevent third parties from triggering the same route manually as often as they like (e.g. to send reminder emails repeatedly or to cause compute time/costs):

export async function GET(request: Request) {
  const authHeader = request.headers.get("authorization");
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response("Unauthorized", { status: 401 });
  }
  // actual cron logic
}

CRON_SECRET is set as a normal environment variable and sent along automatically by Vercel with every actual cron call.

Timeouts and execution limits

Cron routes are subject to the same function timeout limits as the platform’s other serverless/edge functions — a task that takes longer than the configured limit (e.g. sending very many emails in a loop) is forcibly aborted. For longer batch tasks it’s therefore common to use the cron route only as a “trigger” that kicks off an asynchronous background task (e.g. via a queue), instead of doing all the work synchronously within the cron request itself.

Limits by plan

On the free Hobby plan, how often cron jobs can run is limited (e.g. at most once a day per job) — for more frequent intervals (every minute, every hour) a paid plan is needed. In addition, most plans also limit the maximum number of cron jobs that can be configured per project at the same time, which can become relevant for projects with many recurring background tasks.

See also: Vercel, Environment Variables