vercel.json Explained: Rewrites, Redirects, Headers, Crons, and Build Settings
Last updated:
vercel.jsonis Vercel's project configuration file — a strict JSON document at the repo root that controls platform-level behavior: URL rewrites and redirects, HTTP headers, cron jobs, function regions, and build command overrides. Most modern Next.js projects do not need one — Vercel detects everything from package.json plus the framework — but vercel.json is required for non-framework projects, custom routing rules that need to run before the framework, scheduled functions, and any time you need to override the build pipeline. Vercel also supports a newer vercel.tsformat that gives you TypeScript types and dynamic config; the two formats coexist. Prefer the framework's native config (next.config.ts for Next.js routing) when the concern is application-level; reach for vercel.json when the concern is project- or platform-level.
Working on a vercel.json and the deploy keeps failing? Paste it into Jsonic's JSON Validator — it pinpoints trailing commas, missing quotes, and bracket mismatches with exact line numbers.
The minimum vercel.json (and why most projects don't need one)
The smallest valid vercel.json is {}. Vercel reads it, sees no overrides, and falls back to defaults: detect the framework from package.json, run the framework build command, deploy serverless functions from the conventional api/ or framework routes folder. For a vanilla Next.js project, that is the right answer — no file needed at all.
You add vercel.json when you need one specific override. Each top-level key is independent — you can ship a file with just crons or just headers and ignore the rest.
{
"$schema": "https://openapi.vercel.sh/vercel.json"
}The $schema field is optional but recommended — VS Code and other editors will autocomplete and validate every other field against it. It has no effect on the deploy itself; Vercel ignores unknown top-level keys.
When you genuinely need an override, the file stays small. A common shape for a non-Next.js project on Vercel is just a build command plus an output directory:
{
"buildCommand": "vite build",
"outputDirectory": "dist"
}rewrites: server-side URL rewriting (without browser redirect)
rewrites map an incoming URL to a different destination on the server without changing what the browser sees. The user requests /blog/hello, Vercel serves whatever lives at /api/posts/hello, and the address bar still reads /blog/hello. Use rewrites for pretty URLs, proxying to a different backend, and serving the same content under multiple paths.
{
"rewrites": [
{ "source": "/blog/:slug", "destination": "/api/posts/:slug" },
{ "source": "/docs/(.*)", "destination": "https://docs.example.com/$1" },
{
"source": "/api/legacy/:path*",
"destination": "https://old-api.example.com/:path*",
"has": [{ "type": "header", "key": "x-use-legacy", "value": "true" }]
}
]
}Source patterns support three styles:
- Named parameters —
:slugcaptures one segment; reuse it in the destination as:slug. - Catch-all parameters —
:path*captures zero or more segments; useful for proxying deep paths. - Regex captures —
(.*)captures everything; reference with$1,$2, etc. in the destination.
The has field (and its inverse, missing) lets a rewrite fire only when a specific header, cookie, query parameter, or host is present. Conditional rewrites power feature flags, A/B tests, and beta-tier routing.
Rewrites to external URLs proxy the request server-side — the destination origin sees the request, not the browser. This is the simplest way to put a Vercel domain in front of an external API or static host.
redirects: 301/308 redirects with status codes
redirects send a 3xx response back to the browser, which then makes a second request to the new URL. The address bar updates. Use redirects for permanent URL moves, www-to-apex normalization, and any case where the new URL should become the canonical one in caches, bookmarks, and search results.
{
"redirects": [
{ "source": "/about-us", "destination": "/about", "permanent": true },
{ "source": "/old/:slug", "destination": "/new/:slug", "permanent": true },
{
"source": "/(.*)",
"has": [{ "type": "host", "value": "www.example.com" }],
"destination": "https://example.com/$1",
"permanent": true
},
{ "source": "/preview", "destination": "/coming-soon", "permanent": false }
]
}The permanent boolean is shorthand for the status code: true emits a 308 (the modern permanent redirect that preserves the request method), false emits a 307 (modern temporary). For full control, use statusCode directly instead of permanent.
| Status | Meaning | Preserves method? | When to use |
|---|---|---|---|
301 | Moved Permanently (legacy) | No (POST may become GET) | Permanent moves where method change is fine |
302 | Found (legacy temporary) | No | Avoid — use 307 instead |
307 | Temporary Redirect (modern) | Yes | A/B tests, maintenance modes, preview gates |
308 | Permanent Redirect (modern) | Yes | Default for permanent: true — recommended |
Order matters in arrays — earlier rules win. If two redirects could match the same path, list the more specific one first.
headers: cache control, CORS, security headers
headers attach response headers to any path that matches a source pattern. The three most common uses are cache control for static assets, CORS for cross-origin API access, and security hardening headers like CSP and X-Frame-Options.
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=()" }
]
},
{
"source": "/api/(.*)",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "https://app.example.com" },
{ "key": "Access-Control-Allow-Methods", "value": "GET, POST, OPTIONS" },
{ "key": "Access-Control-Allow-Headers", "value": "Content-Type, Authorization" }
]
},
{
"source": "/assets/(.*).(js|css|woff2|png|jpg|webp)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
]
}Cache-Control values worth memorizing:
public, max-age=31536000, immutable— for fingerprinted assets (one-year cache, never revalidate)public, max-age=0, must-revalidate— for HTML pages (always check freshness)private, no-cache— for user-specific responses (no shared caching)s-maxage=86400, stale-while-revalidate=604800— for CDN-cached pages that tolerate slightly stale content
For CORS in production, never ship Access-Control-Allow-Origin: * alongside credentials — browsers will reject the response. Use an explicit origin or echo the request origin from your function instead.
crons: scheduled function invocations
crons tells Vercel to invoke an HTTP route on a schedule. Each entry pairs a path (an API route in your project) with a 5-field cron expression evaluated in UTC. Vercel makes a GET request to the path at the scheduled times.
{
"crons": [
{ "path": "/api/daily-report", "schedule": "0 9 * * *" },
{ "path": "/api/cleanup-expired", "schedule": "*/15 * * * *" },
{ "path": "/api/weekly-digest", "schedule": "0 8 * * 1" }
],
"functions": {
"app/api/daily-report/route.ts": { "maxDuration": 300 }
}
}Cron expression fields (left to right): minute (0–59), hour (0–23), day-of-month (1–31), month (1–12), day-of-week (0–6, Sunday is 0 or 7). Standard operators apply: * matches anything, */N means every N units, 1-5 is a range, 1,3,5 is a list.
As of 2026 the default function timeout is 300 seconds across all plans (Hobby, Pro, Enterprise). That is plenty for nearly all cron workloads — data sync, report generation, cleanup tasks. Use the functions.<route>.maxDuration field only if a job genuinely needs longer.
Securing cron endpoints: Vercel sets the Authorization header on cron invocations to Bearer <CRON_SECRET>, where CRON_SECRETis an environment variable Vercel manages. Verify it inside your handler to reject requests that did not originate from Vercel's scheduler.
Build and output: buildCommand, outputDirectory, framework, installCommand
These four fields override Vercel's auto-detection. For framework projects (Next.js, SvelteKit, Astro, Remix), leave them unset — the framework integration picks the right values. Override only for unusual setups: monorepos with a custom build script, generic static sites that Vercel cannot identify, or pinned framework versions.
{
"buildCommand": "pnpm turbo run build --filter=web",
"outputDirectory": "apps/web/.next",
"installCommand": "pnpm install --frozen-lockfile",
"framework": "nextjs",
"ignoreCommand": "git diff HEAD^ HEAD --quiet ./apps/web"
}buildCommand— the shell command Vercel runs to build. Defaults to the framework's build script (next build,astro build, etc.).outputDirectory— the folder Vercel deploys. Defaults vary by framework (.nextfor Next.js,distfor Vite,buildfor Create React App).installCommand— the install step. Defaults match the lockfile:npm install,pnpm install,yarn install. Override for monorepos with custom workspace setups.framework— explicitly declare the framework slug when auto-detection fails. The framework integration provides defaults for the other build fields and wires up the framework's runtime adapter (Fluid Compute by default).ignoreCommand— a shell command that returns exit code 0 to skip the build (useful for monorepos: skip deploy if no relevant files changed).
Runtime note: as of 2026, Fluid Compute is the default for new projects — your functions run on a Firecracker-based runtime that supports Node.js, Bun, and Rust with the same edge regions as the old Edge runtime, plus full Node.js compatibility. You do not need to opt in. AI Gateway (GA since August 2025) and BotID (GA since June 2025) are platform features configured outside vercel.json.
vercel.ts: the new TypeScript-first alternative
vercel.ts is the newer recommended config format. It uses the @vercel/config package to give you full TypeScript types, comments, imports, and the ability to compute config from environment variables at build time. vercel.json is not deprecated — both formats are supported and the feature set is the same — but new projects and complex configs are easier to maintain in TypeScript.
// vercel.ts
import { defineConfig } from '@vercel/config'
const isPreview = process.env.VERCEL_ENV === 'preview'
export default defineConfig({
// Comments work here — strict JSON doesn't allow them
rewrites: [
{ source: '/blog/:slug', destination: '/api/posts/:slug' },
],
// Conditional config based on environment
redirects: isPreview ? [] : [
{ source: '/old', destination: '/new', permanent: true },
],
crons: [
{ path: '/api/daily-report', schedule: '0 9 * * *' },
],
})| Feature | vercel.json | vercel.ts |
|---|---|---|
| All Vercel config fields | Yes | Yes (same surface) |
| Type safety on field names | Editor only via $schema | Full — typo fails at edit time |
| Comments | No (strict JSON) | Yes (// and /* */) |
| Dynamic config from env vars | No | Yes |
| Import shared route lists from other files | No | Yes |
| Build-time computation | No | Yes |
| Simplest possible file | Yes — pure data | Slightly heavier (one import) |
When to migrate: when your vercel.json has grown past a screenful, when you need conditional config for preview vs production, when you want to share route lists with your application code, or when type errors at edit time would save you from deploy-time failures. When to stay: when the config is short, static, and you value the lowest-possible-friction file.
Coexistence rule: if both files exist in a repo, vercel.ts wins and vercel.json is ignored. Migration is mechanical — rename the file, add the import and defineConfig wrapper, and the existing object becomes the argument.
Key terms
- vercel.json
- Vercel's project configuration file, a strict-JSON document at the repo root. Controls rewrites, redirects, headers, crons, regions, and build overrides. Optional for framework projects that Vercel can auto-detect.
- rewrite
- A server-side URL mapping: the browser keeps the original URL while Vercel serves content from a different path or origin. No status code is sent — the response is whatever the destination returns.
- redirect
- An HTTP 3xx response that tells the browser to fetch a different URL. The address bar updates. Use 308 (permanent) for moved URLs and 307 (temporary) for short-lived rules; both preserve the request method, unlike legacy 301/302.
- cron expression
- A 5-field schedule string (minute, hour, day-of-month, month, day-of-week) evaluated in UTC. Standard operators apply:
*for any,*/Nfor every N units, ranges, and lists. - build command
- The shell command Vercel runs to produce deployable output. Set via
buildCommandinvercel.json, or inferred from the framework integration when omitted. - Fluid Compute
- Vercel's default function runtime as of 2025 — a Firecracker-based microVM model that supports Node.js, Bun, and Rust with edge-region placement and full Node.js compatibility. Replaces the older Edge vs Serverless split for most use cases.
Frequently asked questions
Do I need a vercel.json for a Next.js project?
Most Next.js projects on Vercel do not need a vercel.json at all. Vercel auto-detects the framework from package.json, runs the framework build command, and wires up routing, image optimization, and serverless functions automatically. Reach for vercel.json only when you need something the framework config cannot express: project-level rewrites or redirects that should apply before the Next.js router sees the request, custom HTTP headers for non-route assets, cron jobs, region pinning, or build command overrides for unusual setups. For pure Next.js routing concerns (rewrites between pages, redirects between routes), prefer next.config.ts — it lives with the rest of your app code and ships through the framework. vercel.json is the right tool when the concern is platform-level, not framework-level.
What's the difference between rewrites and redirects in vercel.json?
A rewrite is server-side: the browser keeps the original URL in its address bar while Vercel internally serves the destination. The user never sees that /blog/:slug actually maps to /api/posts/:slug. A redirect sends an HTTP 3xx response back to the browser, which then makes a second request to the new URL — the address bar updates to show the destination. Use rewrites for proxying (serve content from a different path or backend without exposing it), pretty URLs, and A/B routing. Use redirects for permanent URL changes (old paths to new paths after a rename), enforcing canonical hostnames (www → apex), and locale or geography routing where you want the browser to commit to the final URL. Redirects ship a status code (301, 302, 307, 308); rewrites do not — the response status is whatever the destination returns.
How do I add CORS headers in vercel.json?
Use the headers field with a source path pattern and a list of headers. For an API route at /api/* that should accept cross-origin requests, add: { "headers": [{ "source": "/api/(.*)", "headers": [{ "key": "Access-Control-Allow-Origin", "value": "*" }, { "key": "Access-Control-Allow-Methods", "value": "GET, POST, OPTIONS" }, { "key": "Access-Control-Allow-Headers", "value": "Content-Type, Authorization" }] }] }. For production, replace the wildcard with an explicit origin (https://app.example.com) — using * blocks credentialed requests. If your API handles preflight OPTIONS requests itself, you may not need vercel.json headers at all — set them in the function response. The vercel.json approach is better when the headers must apply to static assets or to many routes uniformly.
Can vercel.json have comments?
No. vercel.json is strict JSON (RFC 8259) — it does not allow // line comments, /* block comments */, or trailing commas. Adding any of those breaks the Vercel parser and your deployment fails at build time with a JSON parse error. The standard workarounds are an unused string key (e.g., "_comment": "this field documents the cron schedule") or a separate README in the repo describing the config. If you want a config file that supports comments natively, migrate to vercel.ts — TypeScript supports both // and /* */ comments, and the @vercel/config package gives you typed helpers and the ability to compute config dynamically from environment variables. For richer commenting on JSON in general, see our JSON Comments guide for the workarounds across JSON5, JSONC, and similar formats.
How do I schedule a daily cron job on Vercel?
Add a crons array to vercel.json with one entry per scheduled function. Each entry is an object with a path (the URL of the API route to invoke) and a schedule (a standard 5-field cron expression). For a daily 9 AM UTC run: { "crons": [{ "path": "/api/daily-report", "schedule": "0 9 * * *" }] }. The schedule field uses standard cron syntax — minute, hour, day-of-month, month, day-of-week — and runs in UTC. Vercel invokes the path via HTTP GET; the function should verify the request came from Vercel by checking the Authorization header against the CRON_SECRET environment variable that Vercel provides. As of 2026 the default function timeout is 300 seconds across all plans, so most cron workloads (data sync, cleanup, daily aggregation) fit comfortably without bumping the maxDuration field.
Should I migrate from vercel.json to vercel.ts?
Migrate to vercel.ts when you need any of three things: type safety (the @vercel/config package ships full TypeScript types so misspelled keys fail at edit time, not at deploy time), dynamic config (compute rewrites or headers from environment variables, build-time data, or conditional logic), or comments and imports (TS supports both, plus pulling shared route lists from other files). Stay on vercel.json when your config is static, short, and you want the simplest possible file in your repo — many projects fit this. The two formats support the same feature set today; vercel.ts is the recommended path for new projects and complex configs, but vercel.json is not deprecated and continues to work. If both exist in a repo, vercel.ts wins. Migration is mechanical: rename, add an import, and wrap the existing object in defineConfig.
How do I redirect www to apex domain on Vercel?
You have two options. The simpler one is in the Vercel dashboard under Project → Settings → Domains: add both www.example.com and example.com, then click the redirect arrow next to www to make it redirect to apex (or the reverse). Vercel handles the 308 response at the edge with no config file changes. The vercel.json option is useful when you want the rule to live in code and ship with the deployment: add a redirects entry with a has condition matching the host header. { "redirects": [{ "source": "/(.*)", "has": [{ "type": "host", "value": "www.example.com" }], "destination": "https://example.com/$1", "permanent": true }] }. The has field is what makes the rule host-conditional; without it the redirect would fire on every request regardless of hostname.
What is the function timeout limit on Vercel in 2026?
As of 2026 the default function timeout is 300 seconds (5 minutes) across all plans — Hobby, Pro, and Enterprise. This is a major change from the older limits (10s/60s/90s depending on plan and runtime) that you may still see referenced in stale documentation or older Stack Overflow answers. To extend beyond 300 seconds, set the maxDuration field in vercel.json under functions, or export const maxDuration from your route file: { "functions": { "api/long-task.ts": { "maxDuration": 800 } } }. The hard upper limit varies by plan, but most workloads — webhook handlers, AI Gateway calls, database queries, even longer cron jobs — fit comfortably in 300 seconds without any config. Fluid Compute (the default since 2025) handles concurrent invocations efficiently so timeout settings rarely become the bottleneck.
Further reading and primary sources
- Vercel Docs — Project Configuration — Authoritative reference for every vercel.json field
- Vercel Docs — vercel.ts — TypeScript-first config with @vercel/config — the new recommended path
- Vercel Cron Jobs — Scheduled function invocations with cron expressions and CRON_SECRET verification
- Vercel Routing Middleware — Request interception before cache — works with any framework, complements vercel.json rules
- Vercel Functions — Fluid Compute runtime, maxDuration, regions, and supported runtimes (Node.js, Bun, Rust)