VercelCrondeployment自動化

Vercel cron only runs on the production deployment: how alias set left our schedules two months behind

· · 8 min read
Table of Contents
  1. The symptom: new pages, old schedule
  2. Why an alias is not production
  3. How to check: three IDs must match
  4. How to fix it: promote, but first see which cron jobs it will turn on
  5. The side effect: auto-assign came back on
  6. How we finish a deploy now

Vercel runs cron jobs only on the project's current production deployment, not on whatever your domain happens to point to. If your project has auto-assign of production domains turned off, and you deploy with vercel --prod and then run vercel alias set by hand, the site becomes the new version while the cron jobs stay on the old production deployment. New cron jobs never run, and old ones keep running old code. The fix is vercel promote, followed by an API check that the domain, the production deployment and the cron jobs all point to the same deployment. After promoting we also hit a side effect: auto-assign was switched back on by the system, so the next push to the Git production branch goes straight to production.

This is what happened to ultralab.tw on November 2, with the queries we used and a check script you can run as is.

The symptom: new pages, old schedule

On November 2 we shipped a release that added one new cron job: a daily nurture email for people who had just claimed our starter kit. The deploy followed our usual routine: vercel --prod --yes --archive=tgz, wait for Ready, then vercel alias set for ultralab.tw and www.

Every post-deploy check passed. New pages returned 200, and calling the cron endpoint directly with a dry-run flag also returned 200, so the function itself was fine.

The problem was in the cron list. The new vercel.json defined four cron jobs. Vercel had registered only two, and both pointed at the URL of a deployment from September 3. Looking back, a weekly newsletter cron added in the November 1 release had not been registered either. We had not caught it then.

Put simply: for two months, from September 3 to November 2, our deploys only moved the domain. The site was new, but the deployment Vercel considered production stayed on 9/3. Cron jobs follow production. New ones never ran, and the two old ones kept calling the code on the 9/3 deployment, so any change we made to them in that period never took effect.

It is a classic silent failure. No error, no alert, just something that was supposed to happen and did not.

Why an alias is not production

A Vercel project tracks two separate things:

  1. Which deployment a domain points to. This is what vercel alias set changes.
  2. Which deployment is the project's current production deployment. This is what cron jobs look at.

Normally they move together. With auto-assign of production domains on (the API field is autoAssignCustomDomains), Vercel assigns your production domains to a production build as soon as it is Ready, and makes it the current production deployment at the same time.

Our project had that switch off. With it off, a vercel --prod build is only a staged production build. Alias it by hand and the domain moves, but the production pointer stays where it was.

We could not find out how the switch got turned off. Vercel's docs say an Instant Rollback turns off auto-assignment of production domains, and promoting a deployment restores it. The other common path is someone turning it off on purpose to stage builds before releasing them. If either has happened on your project, it is worth checking.

How to check: three IDs must match

You are comparing three deployment IDs:

  • The deployment your domain points to: deploymentId from /v4/aliases/<domain>
  • The project's production deployment: targets.production.id from /v9/projects/<project>
  • The deployment the cron jobs are registered on: crons.deploymentId in the same response; the host of each entry in crons.definitions is that deployment's URL too

On the day it broke, our domain pointed to the new 11/2 deployment, while production and the cron jobs both pointed to 9/3. Production and the cron jobs agreed with each other, so comparing only those two would have looked fine. You have to bring the domain into the comparison.

Our Vercel CLI, version 50.25.2, did not have vercel crons ls (it replied "not a valid subcommand"; Vercel's docs now list a vercel crons command, so a newer CLI may handle this), so we went straight to the API. This script uses the fetch built into Node 18 and later, no packages needed:

// check-cron-target.mjs
// Usage: VERCEL_TOKEN=xxx node check-cron-target.mjs <project> <team slug> <production domain>
const [project, team, domain] = process.argv.slice(2)
const api = async (path) => {
  const res = await fetch(`https://api.vercel.com${path}?slug=${team}`, {
    headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
  })
  if (!res.ok) { console.error('API', res.status, path); process.exit(2) }
  return res.json()
}
const p = await api(`/v9/projects/${project}`)
const a = await api(`/v4/aliases/${domain}`)
const ids = {
  domain: a.deploymentId,                // where the domain points now
  production: p.targets?.production?.id, // what Vercel considers production
  cron: p.crons?.deploymentId,           // where the cron jobs are registered
}
console.log(ids, 'autoAssign:', p.autoAssignCustomDomains)
for (const d of p.crons?.definitions || []) console.log(' ', d.schedule.padEnd(12), d.path, '@', d.host)
if (new Set(Object.values(ids).filter(Boolean)).size > 1) {
  console.error('Not the same deployment: domain, production or cron is behind')
  process.exit(1)
}

It exits 0 when all three match, 1 when they do not, and 2 on an API error such as a bad token, so you can drop it in as the last step of a deploy script. We ran it once against our own project in the matching state, and once with mocked data for the "domain new, production and cron old" case, where it correctly exits 1.

How to fix it: promote, but first see which cron jobs it will turn on

The fix itself is one line:

vercel promote <deployment-url> --yes --scope=<team slug>

We added --scope because we had to: without it the CLI replied "Deployment belongs to a different team". With the team set, it worked.

We did not run it right away, though. Promote turns on every cron in that deployment's vercel.json, and besides the nurture email, the new build still had the unregistered weekly newsletter. Its schedule is 0 3 * * 0 in UTC, which is every Sunday at 11:00 Taipei time, and it sends real email to subscribers. The next run would have been November 8. That was not part of this release.

So we removed the newsletter entry from vercel.json, deployed again, and promoted that build. Afterwards we queried the API again: all three IDs matched, and the cron list showed the three intended jobs on the new build, without the newsletter.

If this happens to you, read the whole crons block in the new vercel.json before you promote, and ask what each entry will do the moment it starts running. Jobs that were never registered will start running on their schedule right away, without asking.

The side effect: auto-assign came back on

When we deployed again that night, the behavior had changed. As soon as vercel --prod was Ready, the domain was assigned on its own and the log printed Aliased. We ran promote anyway out of habit and got a 409, "already the current production deployment", meaning --prod had already made it production.

The API showed the project's autoAssignCustomDomains was now true, with autoAssignCustomDomainsUpdatedBy set to system. Vercel's docs say that promoting after an Instant Rollback restores auto-assignment; they do not say every promote does. Our case fits that description, but check your own project.

For day-to-day deploys this is good news. Cron jobs now follow --prod with no manual promote. It also adds a risk. The project is still linked to a GitHub repo whose production branch is master, and for historical reasons that repo's master is months old; our main line lives on a different remote. With auto-assign on, a single push to that master would make Vercel build a production deployment and switch the domain to it, replacing the live site with months-old code.

For now a git pre-push hook blocks pushes to that branch. If your project is also "deployed from the CLI, with a Git link left over", check after promoting that the linked production branch really holds the code you want live. If it does not, disconnect the Git link or change the production branch.

How we finish a deploy now

After every production deploy we do two things:

  1. Query the API and confirm the domain, production and cron IDs match. We used to do this by hand; the script above is that check turned into a script.
  2. Compare the number and paths in crons.definitions with vercel.json. They have to be the same.

One more suggestion: for any new or changed cron, wait until its first scheduled time has passed and check that path's logs to confirm it was actually invoked. Registered does not mean it runs.

Problems where "the service looks fine but the work never happens" are the subject of our earlier post on silent failures in automation monitoring. A cron job that never runs is one of the quietest of them.

FAQ

Why isn't my Vercel cron job running?

Check which deployment the cron jobs are registered on. Vercel runs cron jobs only on the project's current production deployment. If auto-assign of production domains is turned off and you deploy with vercel --prod and then run vercel alias set by hand, the domain points to the new build, but the production deployment is still the old one, and the cron jobs keep running the old vercel.json and the old code. Query the project's targets.production and crons.deploymentId through the API and compare them with the deployment your domain points to.

What is the difference between vercel alias set and vercel promote?

alias set only changes which deployment a domain points to. promote makes a deployment the project's production deployment, and cron jobs and the production state follow it. A site that looks new does not mean Vercel considers the new build to be production.

What should I watch out for after promoting?

Two things. First, promote turns on every cron in that deployment's vercel.json at once, so remove any you do not want running yet, such as a weekly email that really goes to subscribers, and deploy again before you promote. Second, on our project auto-assign of production domains was switched back on by the system after we promoted, so the next push to the production branch of the linked Git repo goes live immediately. After promoting, check that switch and make sure that branch holds the code you actually want in production.

Weekly AI Automation Playbook

No fluff — just templates, SOPs, and technical breakdowns you can use right away.

Join the Solo Lab Community

Free resource packs, daily build logs, and AI agents you can talk to. A community for solo devs who build with AI.

Want to try it yourself?

UltraProbe is free and needs no sign-up. One scan tells you whether Google and AI engines can find your site.