Back to Blog
Platforms11 min read

Vercel Cron Jobs: How to Set Up Scheduled Functions

Vercel added native cron job support in 2023, letting you trigger serverless functions on a schedule directly from your project configuration. This guide covers the full setup, the limitations you will hit, and when you should reach for an external cron service instead.

How Vercel Cron Jobs Work

Vercel cron jobs are not a separate service. They are a configuration layer on top of your existing serverless or edge functions. You define a schedule in vercel.json, and Vercel sends a GET request to your specified route at each scheduled time.

Under the hood, Vercel uses its own infrastructure to trigger these requests. The function itself does not "know" it was triggered by a cron schedule unless you check for the CRON_SECRET header. It is just an HTTP GET request to a route in your app.

Step-by-Step Setup

1. Create Your API Route

First, create the serverless function that will run on schedule. In a Next.js App Router project:

// app/api/cron/daily-cleanup/route.ts
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  // Verify the request is from Vercel Cron
  const authHeader = request.headers.get('authorization');
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  try {
    // Your scheduled task logic here
    await cleanupExpiredSessions();
    await purgeOldLogs();

    return NextResponse.json({ success: true, timestamp: new Date().toISOString() });
  } catch (error) {
    console.error('Cron job failed:', error);
    return NextResponse.json({ error: 'Internal error' }, { status: 500 });
  }
}

async function cleanupExpiredSessions() {
  // Delete sessions older than 30 days
  const result = await db.session.deleteMany({
    where: { expiresAt: { lt: new Date() } },
  });
  console.log(`Cleaned up ${result.count} expired sessions`);
}

async function purgeOldLogs() {
  // Remove logs older than 90 days
  const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
  await db.auditLog.deleteMany({ where: { createdAt: { lt: cutoff } } });
}

2. Configure the Schedule in vercel.json

Add the crons array to your vercel.json:

{
  "crons": [
    {
      "path": "/api/cron/daily-cleanup",
      "schedule": "0 3 * * *"
    },
    {
      "path": "/api/cron/send-digest",
      "schedule": "0 9 * * 1"
    },
    {
      "path": "/api/cron/sync-data",
      "schedule": "*/15 * * * *"
    }
  ]
}

The schedule field uses standard 5-field cron syntax. If you are not sure about the expression, use our cron expression generator to build it visually and verify the next execution times.

3. Set CRON_SECRET Environment Variable

To prevent unauthorized access to your cron endpoints, set a CRON_SECRET environment variable in your Vercel project settings. Vercel sends this as a Bearer token in the Authorization header with each cron request.

# Generate a secure random secret
openssl rand -base64 32

# Add to Vercel project settings:
# Settings → Environment Variables → CRON_SECRET

4. Deploy and Verify

Deploy your project. Vercel parses the crons config during deployment and starts scheduling immediately. You can verify the setup in your Vercel dashboard under the project's "Cron Jobs" tab.

Plan Limits: Hobby vs Pro vs Enterprise

The most common frustration with Vercel cron jobs is hitting plan limits. Here is what each plan allows:

FeatureHobby (Free)Pro ($20/mo)Enterprise
Max cron jobs240100
Min interval1 day1 min (but see note)1 min
Function timeout10 s300 s (5 min)900 s (15 min)
Execution included100 GB-hrs1,000 GB-hrsCustom
TimezoneUTC onlyUTC onlyUTC only

Important: On the Hobby plan, you can only run cron jobs once per day or less frequently. If you need a job to run every 15 minutes, you need the Pro plan at $20/month — and that only covers 40 jobs. If you need more, Enterprise starts at custom pricing.

The UTC-Only Constraint

All Vercel cron schedules run in UTC. There is no per-job timezone configuration. If you want a job to run at 9:00 AM Eastern Time, you need to convert that to UTC yourself:

// 9:00 AM Eastern = 14:00 UTC (EST, UTC-5)
// 9:00 AM Eastern = 13:00 UTC (EDT, UTC-4)

// During winter (EST):
{ "schedule": "0 14 * * *" }

// During summer (EDT):
{ "schedule": "0 13 * * *" }

// Problem: You need to update the schedule twice a year
// for daylight saving time. There is no automatic adjustment.

This is a significant limitation for business-hours scheduling. If your daily report must go out at 9:00 AM local time, you will need to redeploy your app twice a year when clocks change, or handle the timezone conversion in your function's logic (check the time on execution and bail out if it is not the right local hour).

CronJobPro lets you set per-job timezones, so "9:00 AM Eastern" stays 9:00 AM Eastern year-round, regardless of DST.

Common Patterns

Here are the most practical use cases for Vercel cron jobs, with code examples:

ISR Revalidation

Force Next.js to regenerate static pages on a schedule:

// app/api/cron/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const auth = request.headers.get('authorization');
  if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Revalidate specific pages
  revalidatePath('/pricing');
  revalidatePath('/blog');
  revalidatePath('/docs');

  return NextResponse.json({ revalidated: true, now: Date.now() });
}

// vercel.json: { "schedule": "*/30 * * * *" }  — every 30 minutes

Database Cleanup

Delete expired records nightly:

// app/api/cron/cleanup/route.ts
export async function GET(request: Request) {
  const auth = request.headers.get('authorization');
  if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const thirtyDaysAgo = new Date(Date.now() - 30 * 86400000);

  const [sessions, tokens, logs] = await Promise.all([
    prisma.session.deleteMany({ where: { expiresAt: { lt: new Date() } } }),
    prisma.verificationToken.deleteMany({ where: { expires: { lt: new Date() } } }),
    prisma.auditLog.deleteMany({ where: { createdAt: { lt: thirtyDaysAgo } } }),
  ]);

  return NextResponse.json({
    cleaned: {
      sessions: sessions.count,
      tokens: tokens.count,
      logs: logs.count,
    },
  });
}

// vercel.json: { "schedule": "0 4 * * *" }  — 4:00 AM UTC daily

Scheduled Email Digest

Send weekly summary emails:

// app/api/cron/weekly-digest/route.ts
export async function GET(request: Request) {
  const auth = request.headers.get('authorization');
  if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const users = await prisma.user.findMany({
    where: { digestEnabled: true },
    select: { email: true, id: true },
  });

  let sent = 0;
  for (const user of users) {
    const activity = await getWeeklyActivity(user.id);
    if (activity.length > 0) {
      await sendDigestEmail(user.email, activity);
      sent++;
    }
  }

  return NextResponse.json({ sent, total: users.length });
}

// vercel.json: { "schedule": "0 9 * * 1" }  — Monday 9 AM UTC
// ⚠️ On Hobby plan, minimum frequency is daily, so weekly works fine

Edge Functions as Cron Targets

Vercel cron jobs can target Edge Runtime functions. This gives you faster cold starts (typically under 50ms) and execution in regions closer to your data. But there are trade-offs:

FeatureServerless (Node.js)Edge Runtime
Cold start~250 ms~0 ms
Max execution300s (Pro)30s
Node.js APIsFull accessLimited (Web APIs only)
npm packagesMost workMust be edge-compatible

Edge functions work well for lightweight cron tasks like cache invalidation or webhook dispatching. They are not suitable for database-heavy operations that need full Node.js compatibility or run longer than 30 seconds.

Monitoring and Debugging

Vercel provides basic cron monitoring in the dashboard. Under your project's settings, the "Cron Jobs" tab shows:

  • A list of configured cron jobs with their schedules
  • Recent execution history with status codes
  • Links to function logs for each execution

What is missing: There are no built-in failure alerts. If a cron job fails at 3:00 AM, you will only discover it by checking the dashboard manually. There is no Slack notification, no email alert, and no dead man's switch pattern. You can build alerting using Vercel's Log Drains (to Datadog, Axiom, etc.), but that requires additional setup and cost.

For debugging, console.log output appears in Vercel's function logs. You can filter by the cron route path to see only cron-related logs.

7 Limitations You Should Know

Before committing to Vercel cron jobs, understand what they cannot do:

  1. UTC only. No per-job timezone support. DST changes require manual schedule updates.
  2. No retries. If the function returns a 500 error, Vercel does not retry it. The execution is logged as failed and that is it. You would need to implement retry logic inside the function itself.
  3. No failure alerts. No built-in notifications when a cron job fails. You need external monitoring.
  4. GET requests only. Cron jobs always trigger a GET request. If your endpoint expects POST with a body, you need a wrapper GET route that calls the POST endpoint internally.
  5. Hobby plan: daily minimum. Free-tier projects cannot schedule jobs more frequently than once per day. This eliminates most real-time monitoring and sync use cases.
  6. Execution time limits. 10 seconds on Hobby, 300 seconds on Pro. If your task takes 6 minutes, you are out of luck even on the paid plan.
  7. No overlap prevention. If a cron job runs every minute but sometimes takes 3 minutes, multiple instances will overlap. You need to implement locking yourself (database lock, Redis lock, etc.).

When to Use an External Cron Service Instead

Vercel cron jobs are convenient for simple cases within a Vercel-hosted project. But the limitations stack up quickly for production workloads. Consider an external cron service when:

  • You need failure notifications. CronJobPro sends Slack, email, or webhook alerts on failure. No Log Drain setup required.
  • You need automatic retries. Failed jobs should be retried 2-3 times before alerting. Vercel does not retry.
  • You need timezone support. Schedule jobs in your local timezone without worrying about DST conversions.
  • You need more than 2 jobs on Hobby. Instead of upgrading to Vercel Pro ($20/month) just for cron, CronJobPro's Pro plan gives you 50 jobs for €3.99/month.
  • You schedule endpoints outside Vercel. If some of your services are on Railway, AWS, or bare metal, an external cron service manages all of them from one dashboard.
  • You want execution history and analytics. Response times, success rates, and trends over time. Vercel shows recent runs but not aggregate analytics.

Hybrid Approach: Vercel + External Cron

Many teams use a hybrid approach. Simple, non-critical tasks run through Vercel's built-in cron. Mission-critical scheduled tasks use an external service for monitoring and reliability. Here is a sensible split:

Use Vercel CronUse External Service
ISR cache revalidationPayment processing / billing
Non-critical cleanupData synchronization with SLAs
Dev/staging environment tasksCustomer-facing email delivery
Low-frequency analyticsHealth checks with alerting

This approach lets you keep simple tasks in your project configuration while ensuring important jobs have proper monitoring and retries. Browse common cron schedules to find the right expression for each job.

Key Takeaways

  • Vercel cron jobs work by sending GET requests to your serverless functions on a schedule defined in vercel.json.
  • Hobby plan limits you to 2 jobs with daily minimum frequency. Pro allows 40 jobs at 1-minute intervals.
  • All schedules run in UTC only. DST-sensitive schedules require manual updates or in-code handling.
  • No built-in retries, failure alerts, or overlap prevention. These must be implemented manually or with external tools.
  • For production workloads that need reliability guarantees, an external cron service like CronJobPro fills the gaps.

Related Articles

Need more than Vercel's built-in cron?

CronJobPro gives you automatic retries, Slack/email alerts, timezone support, and a visual dashboard — starting at €3.99/month for 50 jobs. Works with any HTTP endpoint, including Vercel functions.