TL;DR

Vercel Cron triggers HTTP GETs (or POSTs) on a schedule. Rotor schedules + HTTP callback mode replace it — with no 1-cron-per-day Hobby cap and no per-project Pro ceiling. Your existing /api/cron/* handlers work as-is; Rotor becomes the scheduler that calls them.

At Rotor Free ($0) you get unlimited schedules against a 10k/mo execution budget — vs. Vercel Hobby's hard 1 cron per day limit.

Pricing at a glance

PlanPriceSchedule limitsNotes
Vercel Hobby$01 cron per dayDaily granularity only
Vercel Pro$20/moEvery minutePer-project Vercel plan cost
Rotor Free$0Unlimited schedules10k executions/mo total
Rotor Pro$19/moUnlimited schedules100k executions/mo (covers most cron use cases)
Rotor Team$99/moUnlimited schedules1M executions/mo
Tip

Key differentiator: Rotor Free lets you run 10,000 scheduled executions per month across as many schedules as you want. Vercel Hobby caps you at 1 cron per day regardless of execution volume. If you're hitting the Hobby cron cap, Rotor Free is a drop-in replacement at the same price ($0).

Note

Competitor pricing was verified against vercel.com/pricing on the publish date of this guide. Vercel frequently adjusts their cron allowances — re-read their pricing page before making a purchasing decision.

Architecture mapping

Vercel Cron conceptRotor equivalentMigration notes
vercel.json crons[] entryPOST /v1/schedulesOne schedule per entry
Handler route (e.g. /api/cron/cleanup)Queue with callback_urlSame handler — just attach its URL to a Rotor queue
CRON_SECRET bearer headerX-Rotor-Signature HMAC verificationSwap the auth check; handler body stays the same
Default UTCExplicit timezone requiredRotor schedules MUST declare a timezone — see below
Per-project isolationPer-workspace isolationWorks across any deploy target (Vercel, Railway, Fly, self-hosted)

No handler-side changes required: Vercel Cron already hits an HTTP route on your app. Rotor does the same thing — you just change what authenticates the incoming request (HMAC instead of bearer).

Before / After

{
  "crons": [
    {
      "path": "/api/cron/cleanup",
      "schedule": "0 */6 * * *"
    },
    {
      "path": "/api/cron/daily-digest",
      "schedule": "0 12 * * *"
    }
  ]
}

Delete the crons array from vercel.json once the Rotor schedule is live. Your handler route stays exactly where it is.

Timezone — read this first

Warning

Rotor schedules REQUIRE an explicit timezone parameter (API-12). Vercel Cron defaults to UTC silently — when you migrate, decide what timezone you want.

  • If your existing cron was "run at 12:00 every day" and you intended that in UTC, set "timezone": "UTC".
  • If you intended local business hours (e.g. "noon eastern"), set "timezone": "America/New_York" — Rotor will handle DST.

Pick wrong and your reports will fire an hour off twice a year.

Supported timezone values: any IANA zone name (e.g. UTC, America/New_York, Europe/London, Asia/Tokyo). See the full list at IANA tz database.

Signature verification

Vercel Cron authenticates callbacks via the CRON_SECRET env var + bearer header:

// Before: Vercel Cron handler
export async function GET(req: Request) {
  const auth = req.headers.get("authorization");
  if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response("unauthorized", { status: 401 });
  }
  // ...do cron work
}

Rotor authenticates via HMAC signature on the request body — stronger because it binds the signature to the exact payload, preventing replay with altered bodies. See the full Webhook Signature Verification reference for Node, Python, Go, and Ruby.

// After: Rotor callback handler
import { createHmac, timingSafeEqual } from "node:crypto";
 
export async function POST(req: Request) {
  const body = await req.text();
  if (!verifyRotorSignature(req.headers, body)) {
    return new Response("unauthorized", { status: 401 });
  }
  const jobId = req.headers.get("x-rotor-job-id");
  // ...do cron work (same as before); use jobId for idempotency if needed
  return new Response("ok");
}
 
function verifyRotorSignature(headers: Headers, body: string): boolean {
  const id = headers.get("x-rotor-job-id") ?? "";
  const ts = headers.get("x-rotor-timestamp") ?? "";
  const sig = headers.get("x-rotor-signature") ?? "";
  if (!id || !ts || !sig) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
 
  const expected = `v1,${createHmac("sha256", process.env.ROTOR_CALLBACK_SECRET!)
    .update(`${id}.${ts}.${body}`)
    .digest("base64")}`;
  const a = Buffer.from(expected);
  const b = Buffer.from(sig);
  return a.length === b.length && timingSafeEqual(a, b);
}

Request method changes: Vercel Cron sends GET. Rotor sends POST with a JSON body (empty {} for schedules with no payload). Update your route handler from GET to POST — or accept both if you want to leave Vercel Cron running in parallel during migration.

Auth migration cheatsheet

CheckVercel CronRotor callback
Header readauthorization: Bearer $CRON_SECRETx-rotor-signature: v1,<base64>
Secret sourceprocess.env.CRON_SECRETprocess.env.ROTOR_CALLBACK_SECRET (minted once via PATCH /v1/queues/:name)
Replay windowNone±300s (checked against x-rotor-timestamp)
Idempotencyx-rotor-job-id — stable across retries
Request methodGETPOST

Automated import

We ship a small script that reads your vercel.json, extracts the crons[] array, and emits the Rotor queue + schedule configuration as JSON-lines.

# Run it
npx tsx docs/scripts/import-from-vercel-cron.ts ./vercel.json

Example input and output:

// vercel.json
{
  "crons": [
    { "path": "/api/cron/cleanup", "schedule": "0 */6 * * *" },
    { "path": "/api/cron/daily-digest", "schedule": "0 12 * * *" }
  ]
}
// stdout — one object per line
{"type":"queue","config":{"name":"cron-cleanup","callback_url":"https://YOUR_DOMAIN/api/cron/cleanup"}}
{"type":"schedule","config":{"queue":"cron-cleanup","cron":"0 */6 * * *","timezone":"UTC"}}
{"type":"queue","config":{"name":"cron-daily-digest","callback_url":"https://YOUR_DOMAIN/api/cron/daily-digest"}}
{"type":"schedule","config":{"queue":"cron-daily-digest","cron":"0 12 * * *","timezone":"UTC"}}

After running the script:

  1. Search-and-replace YOUR_DOMAIN with your actual production host (e.g. your-app.vercel.app).
  2. Replay the type:queue lines as POST /v1/queues + PATCH .../callback_url.
  3. Replay the type:schedule lines as POST /v1/schedules.
  4. Verify one scheduled execution fires and hits your handler, then remove the crons array from vercel.json.

Next steps

  1. Start a Pro trial — 14 days, no credit card.
  2. Install the SDK: pnpm add @rotorsh/sdk — see the Node.js quickstart.
  3. Join the Anthropic Discord#rotor channel for migration help.