Skip to main content

Cron Jobs

Scheduled tasks that run automatically on Vercel.

Cron jobs are scheduled HTTP requests that Vercel fires on a timetable. DirectoryKit ships with two.

What's scheduled

vercel.json
"crons": [
  { "path": "/api/cron/competitions",     "schedule": "15 8 * * *" },
  { "path": "/api/cron/winner-reminders", "schedule": "0 9 * * *" }
]
EndpointWhenWhat it does
/api/cron/competitions08:15 UTC dailyCloses competitions that have ended, picks winners, triggers notifications
/api/cron/winner-reminders09:00 UTC dailyEmails winners who haven't yet claimed their prize

Both paths are regular Next.js API routes under app/api/cron/. You can run them manually with curl — useful for testing.

Vercel Cron Jobs settings

How cron URLs are protected

Anyone on the internet could hit https://your-site.com/api/cron/competitions otherwise. To prevent that, every cron endpoint checks this header:

Authorization: Bearer ${CRON_SECRET}

Vercel automatically adds the header when it calls your cron endpoints, using the CRON_SECRET env var. If CRON_SECRET is missing or wrong → the endpoint returns 401 Unauthorized.

Generating a good CRON_SECRET

openssl rand -hex 32

Paste that into both .env.local and Vercel → Settings → Environment Variables.

Vercel plan limits

PlanCron rate limit
Hobby (free)1 cron, runs up to once per day
ProMultiple crons, any schedule
EnterpriseNo limits

If you're on Hobby, the default config will hit the limit. Either:

  • Keep one cron and disable the other
  • Upgrade to Pro

Reading cron schedules

The format is standard Unix cron: min hour day month weekday. Examples:

ExpressionWhen
0 * * * *Every hour on the hour
*/15 * * * *Every 15 minutes
0 9 * * *09:00 UTC daily
0 9 * * 109:00 UTC every Monday
0 0 1 * *Midnight UTC on the 1st of each month

Use crontab.guru to sanity-check.

UTC, always

Vercel cron schedules are UTC. Convert from your local time before scheduling.

Adding a new cron job

1. Create the endpoint

app/api/cron/my-task/route.ts
import { NextRequest } from 'next/server';
 
export async function GET(req: NextRequest) {
  // Verify the request is from Vercel cron
  const auth = req.headers.get('authorization');
  if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }
 
  // Do your work
  // ...
 
  return Response.json({ ok: true });
}

2. Register the schedule

Edit vercel.json:

"crons": [
  { "path": "/api/cron/my-task", "schedule": "0 */6 * * *" }
]

3. Deploy

Vercel reads vercel.json on deploy and wires the schedule. Changes to schedules require a redeploy.

AI Prompt· Add a new cron job

Add a new cron job to this DirectoryKit project.

Details:

  • Endpoint path: /api/cron/{your-task}
  • What it does: {1 sentence — e.g. "expires promotions whose end_date has passed and refunds unused time"}
  • Schedule: {e.g. "every day at 3am UTC" or give a cron expression}
  • What it queries / writes: {tables and fields}
  • What it emails or notifies (if any): {recipients, type}

Please:

  1. Create app/api/cron/{your-task}/route.ts following the existing pattern in app/api/cron/.
  2. Enforce Authorization: Bearer $\{CRON_SECRET\} and return 401 otherwise.
  3. Use the db singleton from @/lib/supabase/database for DB ops.
  4. Add the schedule to vercel.json's crons array. Warn me if I'm on the Hobby plan limit (max 1 cron/day).
  5. Tell me how to test it with curl + CRON_SECRET locally.

Follow CLAUDE.md conventions.

Testing locally

Manually hit your endpoint with the secret:

curl -H "Authorization: Bearer $CRON_SECRET" \
  http://localhost:3000/api/cron/competitions

It should return { "ok": true } (or whatever your handler returns).

Monitoring

Vercel Settings → Cron Jobs shows each cron's last run status and logs. Failures appear in your project's Logs tab with a cron- prefix.