Vercel Monitoring: 6 Failure Modes Behind a Green Deploy
The deploy finished. The dashboard showed a green checkmark, the preview URL looked fine, and everyone moved on. Forty minutes later a customer in Sydney reported checkout errors. Production had been returning 500s on one route since the promotion, and the first anyone heard of it was a support ticket.
That gap is what Vercel monitoring is really about. Vercel abstracts away servers, but it doesn't abstract away failure. It moves failure into places many teams never look: build logs, runtime function logs, cache headers, DNS records, and a usage page that only gets opened when the invoice arrives. The platform tells you a deployment succeeded. It doesn't tell you the deployment works.
This guide walks through the six things that most often go wrong on Vercel projects, where each one surfaces (deployment view, runtime logs or the Observability tab), one concrete inspection step for each, and how long triage usually takes when you know where to look.
This is part one of a three-part series. Part two is a hands-on guide to Vercel triage with native tools only: CLI, log drains, checks and rollbacks. Part three covers putting an AI agent on top of your Vercel deployments and logs.
01Build
Failed or stuck
Errors only reproducible in the build container, or a queue that never drains.
02Config
Env var drift
A key set in Preview and missing in Production.
03Runtime
Function errors
Crashes, timeouts and cold starts that only real traffic triggers.
04Cache
ISR surprises
Stale pages that should be fresh, dynamic pages that should be static.
05Edge
Domains and certs
A DNS or CAA change that takes the site down with every deploy green.
06Cost
Usage spikes
Crawlers, loops and misconfigured caching that arrive as an invoice.
Why a green deploy tells you so little
None of this is a Vercel flaw. It follows from how the platform is built.
Deployment status describes the build, not the traffic. A deployment is Ready when the build finished and the output was uploaded. Whether the first real request to /api/checkout succeeds is a separate question that the status never answers.
Every signal is pull-based. Runtime logs, cache headers, environment diffs, domain status and usage all exist, each on its own surface. None of them pushes to you by default. Someone has to go look.
Retention is short. Runtime logs in the dashboard last an hour on Hobby and a day on Pro unless you drain them. If nobody was watching when the errors started, the evidence may be gone by the time someone is.
Config and code ship separately. Environment variables, domains and spend settings change in the dashboard, outside your git history. A deploy can break because of something nobody committed.
0114:02
Deploy Ready
Build passes, green checkmark, preview looked fine.
0214:03
Promoted
Production alias moves. Nothing else changes in the dashboard.
0314:05
500s begin
One route starts failing. Runtime logs record it. Nobody is looking.
0414:45
Support ticket
A customer reports checkout errors. Triage starts here.
1. Failed and stuck deployments
What it looks like. A push to main and no new production deployment. Either the build errored (a type error that only reproduces with production env vars, a dependency that resolves differently in Vercel's build container, an out-of-memory kill during next build), or the deployment sits in Building or Queued far longer than usual, often because concurrent builds on your plan are saturated.
Where it shows up. The project's Deployments view in the dashboard, with the full build output under the failed deployment's Build Logs. From a terminal:
vercel ls # recent deployments and their states
vercel inspect <deployment-url> --logs # full build log for one deployment
One inspection step. Read the build log bottom-up. Vercel prints the failing command's output last, so the real error is almost always in the final 30 lines, above the Error: Command "next build" exited with 1 line. Vercel's build troubleshooting guide lists the common exit reasons.
Typical triage time. 5 to 15 minutes when the build log states the error plainly. 30 to 60 minutes when the build passes locally and fails only in Vercel's environment, which is usually failure mode 2.
2. Environment variable drift between environments
What it looks like. The classic: works in preview, breaks in production. Vercel keeps separate variable sets for Development, Preview and Production, so a variable added on a feature branch quietly never gets a production value. The failure often isn't a build error. It's a runtime 500 on the first request that touches the missing key, or worse, an app silently pointed at a staging database.
Where it shows up. Nowhere proactive. It shows up as runtime errors in the logs (mode 3) unless you diff the environments yourself under Settings → Environment Variables.
One inspection step. Compare the lists per environment:
vercel env ls production
vercel env ls preview
Any variable present in preview but missing from production is a candidate for the failure you're chasing. The environment variables docs explain how targeting works, including branch-specific values, which are another common source of drift.
Typical triage time. Two minutes once you suspect it. Hours when you don't, because the error message ("cannot read properties of undefined") points at your code, not your config.
3. Runtime function errors and cold-start latency
What it looks like. The deploy is green because nothing at build time was wrong. Then real traffic hits a function: an unhandled exception on a specific input, an upstream API timeout, or FUNCTION_INVOCATION_TIMEOUT on a route that occasionally runs past its duration limit. Separately, p99 latency spikes that users describe as "sometimes it just hangs" are often cold starts. A function with a large bundle or many dependencies can add noticeable startup time to the first request after idle.
Where it shows up. The project's Logs tab shows runtime output with level and route filters. The Observability tab aggregates error rate, invocation counts and duration percentiles per route (observability docs). From the CLI:
vercel logs <deployment-url>
Given a deployment URL, vercel logs follows the stream live, each invocation for up to five minutes. Run it bare in a linked repo to query request logs from the last 24 hours. Anything older than your plan's retention window is gone unless you set up a log drain (covered in part two).
One inspection step. In Observability, sort routes by error count for the window since your last deploy. If one route dominates and its first error timestamp matches a deployment, you have your suspect commit. For latency, compare a route's p75 with its p99. A wide gap with a low invocation rate is the cold-start signature. A uniformly slow route is your code or an upstream.
Typical triage time. 10 to 30 minutes for a crashing route with clear stack traces. Half a day for intermittent timeouts, because you're reconstructing behavior from whatever logs happened to be kept.
4. ISR and cache behavior surprises
What it looks like. Content that should have updated didn't (a price change that shipped an hour ago still shows the old value for some users), or the reverse: a page you believed was static renders on every request and burns function invocations. Incremental Static Regeneration serves stale content by design while it revalidates in the background (ISR docs). That's excellent until nobody remembers which pages carry which revalidate interval.
Where it shows up. Not in any error log, because nothing is failing. The truth is in the response headers.
One inspection step. Ask the edge directly:
curl -sI https://your-site.com/pricing | grep -i x-vercel-cache
HIT means served from cache, STALE means served stale while revalidating, and MISS or BYPASS mean the request went to your function. A page that should be static returning MISS on repeated requests means something (a cookie read, a dynamic export, an uncached fetch) opted it out of caching.
Typical triage time. Five minutes to confirm cache state from headers. Longer to find which code path opted the route out, since Next.js can switch a route to dynamic rendering from deep inside a component tree.
5. Domain and certificate issues
What it looks like. The apex domain works and www doesn't, or the reverse. A certificate renewal silently fails and browsers start warning users. A DNS change at the registrar breaks the records that made everything work. These are rare per project but severe when they hit, because the site goes down while every deployment stays green.
Where it shows up. Settings → Domains in the project dashboard flags invalid configurations and pending certificates. From the CLI:
vercel certs ls
dig +short CNAME www.your-site.com
One inspection step. Check the domain's status in Settings → Domains first. Vercel states exactly which record it expects and what it sees. The most common renewal blocker is a CAA record that doesn't authorize Vercel's certificate authority. The domains troubleshooting docs cover the required records.
Typical triage time. Ten minutes to diagnose. Anywhere from minutes to 48 hours to resolve, depending on DNS propagation and who controls the registrar account.
6. Usage spikes that become bill spikes
What it looks like. A crawler discovers your image-heavy catalog and image optimization transformations jump 40x. A middleware bug triggers an invocation loop. An ISR misconfiguration (mode 4) turns a static page into a function call per request. None of this pages anyone. It accrues quietly and arrives as an invoice several times your normal spend.
Where it shows up. The team-level Usage page breaks spend down by resource (function invocations and duration, edge requests, image optimization, data transfer), and the Observability tab shows which routes and assets drive it.
One inspection step. Turn on Spend Management (Pro plans): set a monthly spend amount, get notified at thresholds, and optionally pause projects when the cap is hit. Then look at Usage weekly, not monthly. A spike caught on day 2 is an annoyance. On day 28 it's a budget conversation.
Typical triage time. 15 to 30 minutes to find the resource and route driving a spike. The expensive part is the days it ran before anyone looked.
One incident, read properly
Go back to the Sydney ticket. Production started returning 500s on /api/checkout at 14:05. There are three reasonable readings, and each predicts something different.
- A code regression in the new deploy. The errors start within minutes of the promotion, on the routes the new commit touched, with a stack trace into your code.
- Environment drift. The errors also start at the promotion, but the message is about an undefined value or a refused connection, and the key involved exists in Preview and not in Production.
- An upstream failure. The errors start at a time unrelated to any deploy and the message is a timeout or 5xx from a third-party API.
Each one has a check:
# When did production change?
vercel ls my-app --environment production
# What are the errors, and on which routes?
vercel logs my-app.vercel.app --json \
| jq 'select(.responseStatusCode >= 500) | {path: .requestPath, msg: .message}'
# Is the key the error names set in production at all?
vercel env ls production | grep -i PAYMENTS
In this case the promotion landed at 14:03, the errors were all on /api/checkout with PAYMENTS_WEBHOOK_SECRET is undefined, and vercel env ls production didn't list the key. Reading two. The fix is adding the variable and redeploying, or rolling back while you do. A rollback alone would have "worked" and hidden the cause until the next deploy.
That took three commands and the knowledge of which three to run. Without someone watching, it took forty minutes and a customer to start.
The common thread: green isn't the same as healthy
Only one of the six failure modes (the failed build) is visible in the deployment status teams actually watch. The other five hide in runtime logs that don't persist by default, headers nobody curls, environment diffs nobody runs, DNS records nobody rechecks, and a usage page nobody opens. Vercel gives you the signals, scattered across five surfaces and all pull-based.
| What most teams watch | What the failure needs | |
|---|---|---|
| Failed build | Deployment status | Deployment status (covered) |
| Env var drift | Nothing | A diff across environments, per deploy |
| Function errors | Support tickets | Error rate per route, tied to the deploy |
| Cache behavior | Nothing | Headers on the routes that matter |
| Domains and certs | Nothing | Domain status and certificate expiry |
| Usage spikes | The invoice | Usage by resource, weekly or better |
That's workable at two deploys a week. At twenty deploys a week across a handful of projects, "someone goes and looks" is the monitoring strategy that produced the Sydney ticket.
The next step is wiring these surfaces together yourself. Part two does exactly that with native tools only: CLI inspection, log drains for persistence, deployment checks, webhooks and instant rollbacks.
Make the signals come to you
Everything in this guide is pull-based. The fix is making it push-based, and adding judgment. CloudThinker connects to Vercel with a scoped read-only token, watches deployments and runtime errors, and ties a 500 spike to the deploy that introduced it. It proposes a rollback or an env fix instead of handing you another dashboard. It acts within Auto Mode, so you decide what runs on its own and what waits for approval.
A first question to try:
"checkout-api started returning 500s after the last production deploy. Compare the error start time with recent promotions, check whether any variable is set in Preview but missing in Production, and tell me which explanation fits. Propose only, change nothing."
Try CloudThinker free with 100 premium credits and no card required, or continue with the DIY guide.
