Skip to content
Skip to main content
JSON and TOML logos side by side on a charcoal desk mat between blank index cards and folded paper, illustrating the vercel.json vs vercel.toml project configuration choice on Vercel
8 min readBy Carlos Aragon

vercel.toml vs vercel.json: What I Found on Real Deploys

vercel.toml is a drop-in replacement for vercel.json: same properties, same routing behavior, plus comments. The catch is that TOML has no null, you can only keep one of the two files, and a Vercel CLI older than 58.0.0 will silently ignore it on local builds. Vercel announced it yesterday. This morning I deployed it to throwaway projects, put both files in the same repo on purpose, built it with an old CLI, and fed it broken TOML. Here's what each of those actually does.

What Is vercel.toml?

It's a third way to write project configuration on Vercel, next to vercel.json and the programmatic vercel.ts. The November 2 changelog says it supports "all of the same configuration properties" as vercel.json, with comments and a flatter structure. The docs page is two examples long, so most of what follows came from testing.

It isn't brand new code, either. Digging through the Vercel CLI repo, TOML support landed behind a VERCEL_TOML_CONFIG_ENABLED flag in April 2026, and vercel@58.0.0 (July 27, 2026) made it always on. The CLI parses the file with smol-toml 1.5.2, which matters later when we get to parse errors. The latest CLI as of this morning is 65.10.0.

Does vercel.toml Behave Exactly Like vercel.json?

For the rules I tested, yes. I deployed a small static site with this config using CLI 65.10.0:

"$schema" = "https://openapi.vercel.sh/vercel.json"

# strip .html from URLs
cleanUrls = true
trailingSlash = false

# temporary redirect (307)
[[redirects]]
source = "/old"
destination = "/about"
permanent = false

# permanent redirect (308)
[[redirects]]
source = "/legacy/:path*"
destination = "/:path*"
permanent = true

[[rewrites]]
source = "/api/data"
destination = "/api-mock/data.json"

[[headers]]
source = "/(.*)"

  [[headers.headers]]
  key = "X-Frame-Options"
  value = "DENY"

Then I curled every rule on the live deployment:

/about.html     308  location: /about
/about/         308  location: /about
/old            307  location: /about
/legacy/about   308  location: /about
/api/data       200  (rewrite served the JSON file)
/               200  x-frame-options: DENY

That's the same output you'd get from the equivalent vercel.json. Clean URLs gave a 308, the permanent = false redirect gave a 307, the path pattern carried through, and the rewrite served the file without changing the URL. Custom headers showed up on the 200 responses (not on the redirects, which is normal Vercel behavior). The deploy itself took about 17 seconds.

The one thing that will trip you up is headers. The JSON property is called headers at two levels, so in TOML each rule is a [[headers]] table and each header inside it is a [[headers.headers]] table. It works, it just reads strangely the first time. Indenting the inner tables, like I did above, is allowed and helps.

Function settings get nicer, though. A dotted key like functions."api/*.js".maxDuration = 60parses to exactly the nested object vercel.json wants. If you're still fighting runtime config there, my post on the Node 20 discontinued error covers the other half of that file.

What Happens If vercel.json and vercel.toml Both Exist?

The deploy fails. Which machine catches it depends on your CLI version. I copied the working site, added a one-line vercel.json, and deployed it twice:

$ npx vercel@65.10.0 deploy --prod
Error: Multiple config files found: vercel.json, vercel.toml.
Please use only one configuration method.

$ vercel deploy --prod          # Homebrew CLI 50.15.0
Building...
Error: Multiple configuration files found: vercel.toml, vercel.json.
Please use only one.

The new CLI stops before uploading anything (it didn't even create the project). The old one uploads, starts the build, and the build on Vercel fails. Either way there's no precedence rule where one file quietly wins, which I prefer. The practical risk is a half-finished migration: add vercel.toml in one PR, forget to delete vercel.json, and every deploy on that branch breaks. Do both in the same commit.

The source code also shows now.jsonis now a hard error of its own ("deprecated and no longer supported"), so if you have a very old project, that file has to go too.

Will an Old Vercel CLI Ignore vercel.toml?

Sometimes, and that's the most dangerous finding of the morning. The Homebrew CLI on my Mac mini is 50.15.0, from before the flag was removed. Two different results:

  • Remote build (vercel deploy without --prebuilt):all rules applied. The build runs on Vercel's side, and that side reads vercel.toml. Git deployments work the same way, so a plain GitHub-connected project is fine.
  • Local build (vercel build, then deploy --prebuilt): vercel.toml was ignored. The generated .vercel/output/config.json had 2 routes instead of the 10 that CLI 65.10.0 produced, and the deploy succeeded with no warning.

Here's that prebuilt deploy on the live URL:

# vercel build + vercel deploy --prebuilt on CLI 50.15.0
/               200
/about          404   <- cleanUrls gone
/about.html     200
/old            404   <- redirect gone
/api/data       404   <- rewrite gone
/vercel.toml    200   <- your config file, served publicly

Every redirect, rewrite and header gone, and the config file itself published as a static asset. With the new CLI, /vercel.toml returned 404 as it should. If your CI builds with --prebuilt (common when you run tests and the build in one GitHub Actions job, like in my Claude Code in GitHub Actions setup), pin the Vercel CLI to 58.0.0 or newer before you switch formats.

One more wrinkle that almost made me miss this. My first old-CLI build looked fine, because CLI 65.10.0 had already run in that folder and written a compiled copy of the config to .vercel/vercel.json. The old CLI picked that file up. Once I deleted it, the old CLI dropped the rules. .vercel/is normally gitignored, so a fresh CI checkout won't have that copy. Your laptop might, which is how "works on my machine" happens.

What Can't vercel.toml Express?

Null.TOML doesn't have it. In the vercel.json reference, seven properties are typed string | null or boolean | null: buildCommand, devCommand, fluid, framework, ignoreCommand, installCommand and outputDirectory. The docs say "framework": null is how you pick the "Other" preset. Writing framework = nullin vercel.toml fails to parse with "invalid value."

It gets worse if you convert automatically. smol-toml's stringify turned {"framework": null, "cleanUrls": true} into just cleanUrls = true. No error. The key vanished, so Vercel would fall back to framework detection, which is a different build. If your vercel.json has a null anywhere, keep that setting in Project Settings, or stay on vercel.json.

The other parser rules I hit, all from the same TOML 1.0 behavior:

  • Inline tables must stay on one line. The docs' own destination = { service = "backend" } is fine. Splitting it across lines fails.
  • No trailing commas inside inline tables.
  • Duplicate keys are an error("trying to redefine an already defined table or value"). JSON parsers usually let the last one win, so this can surface bugs your vercel.json was hiding.
  • $schema needs quotes because $isn't allowed in a bare key.

The good news is the error messages are specific. This is what the CLI printed for a multiline inline table:

Error: Failed to parse vercel.toml: Invalid TOML document:
newlines are not allowed in inline tables
35:  source = "/x"
36:  destination = {
                    ^
37:    service = "backend"

All Six Tests at a Glance

TestResult
Only vercel.toml, CLI 65.10.0 deployAll rules applied, deploy in ~17 s
Only vercel.toml, CLI 50.15.0 deploy (remote build)All rules applied
Both files, CLI 65.10.0Refused before upload, no project created
Both files, CLI 50.15.0Uploaded, build failed on Vercel
vercel build on CLI 50.15.0 + --prebuiltvercel.toml ignored: 2 routes instead of 10
Multiline inline tableParse error with line pointer

How Do I Convert vercel.json to vercel.toml Safely?

Use the same parser the CLI uses, refuse nulls, and check the round trip before writing anything. I ran this script against my test config and against a file with framework: null (it exited 1 and named the key):

// json2toml.mjs   usage: node json2toml.mjs vercel.json > vercel.toml
// npm i smol-toml   (the same parser the Vercel CLI bundles)
import { readFileSync } from "node:fs";
import { parse, stringify } from "smol-toml";

const config = JSON.parse(readFileSync(process.argv[2], "utf8"));

// TOML has no null. stringify() silently drops null keys,
// which changes meaning ("framework": null = "Other" preset).
const nulls = [];
(function walk(v, path) {
  if (v === null) nulls.push(path);
  else if (typeof v === "object")
    for (const [k, x] of Object.entries(v)) walk(x, `${path}.${k}`);
})(config, "$");
if (nulls.length) {
  console.error(`null values can't be expressed in TOML: ${nulls.join(", ")}`);
  process.exit(1);
}

const toml = stringify(config);
if (JSON.stringify(parse(toml)) !== JSON.stringify(config)) {
  console.error("round-trip mismatch, not writing");
  process.exit(1);
}
process.stdout.write(toml);

Then the migration itself is short:

  1. Run vercel --version wherever you build. Anything using vercel build needs 58.0.0 or newer.
  2. Convert with the script above and add comments where they earn their place.
  3. Delete vercel.json in the same commit.
  4. Deploy a preview and curl the redirects and headers you care about. Compare status codes and Location values to production.

Should You Switch? vercel.toml vs vercel.json vs vercel.ts

My take: don't migrate for the sake of it.A 15-line vercel.json gains nothing. TOML pays off when the file gets long and needs explanations: a few hundred redirects from a site migration, a Services setup with a frontend and a Python backend, security headers where you want to note why each one exists. Comments are the whole feature, and they're a good one. JSON never let you leave a note saying "don't remove this redirect, the 2024 newsletter links here."

  • vercel.json: you need a null value, or other tools in your pipeline read or write the file directly.
  • vercel.toml: long static config you want to comment, and every build path runs CLI 58 or newer.
  • vercel.ts:config that changes with environment variables or build-time logic. If you're already deep in TypeScript, as in my Workflow DevKit agents, it's the more natural fit.

For this site I'm staying put. It's a Next.js app whose couple of redirects live in next.config.ts, so there's no vercel.json to replace. Where I'd reach for TOML first is a client migration with a long redirect map, where every rule needs a note.

Frequently Asked Questions

Can I use vercel.toml and vercel.json at the same time?

No. Vercel CLI 65.10.0 refused to deploy with 'Multiple config files found: vercel.json, vercel.toml. Please use only one configuration method.' An older CLI uploaded the files anyway, and the build on Vercel failed with a similar error. Pick one file and delete the other in the same commit.

Does vercel.toml support everything vercel.json does?

Every property, yes, but not every value. TOML has no null, so settings like framework = null, which selects the Other preset, can't be written in vercel.toml. Set those in Project Settings or keep vercel.json.

Which Vercel CLI version supports vercel.toml?

vercel@58.0.0, released July 27, 2026, removed the feature flag and made it always on. Git deployments and remote CLI builds read it regardless of your local CLI, because the build runs on Vercel. Local vercel build and --prebuilt deploys need 58 or newer.

How do I write custom headers in vercel.toml?

Each rule is a [[headers]] table with a source, and each header inside it is a nested [[headers.headers]] table with key and value. It looks odd because the JSON property is also called headers at both levels.

Should I switch from vercel.json to vercel.toml?

Switch if your config is long enough that comments would help, like a big redirect list or a Services setup. Stay on vercel.json if you rely on null values or tooling that edits vercel.json. Use vercel.ts if the config has to change per environment.

How I Tested This

November 3, 2026, from a Mac mini in Allen, Texas. Vercel CLI 65.10.0 (via npx) and 50.15.0 (Homebrew), deploying a static HTML site to three throwaway projects on my own Vercel team, each deleted afterwards through the API. Every result above is a curl against the live deployment or the generated .vercel/output/config.json. Parser behavior was checked against smol-toml 1.5.2, the version bundled in the CLI, and the CLI history comes from the PR that removed the flag. I didn't test Services or Next.js projects, only static routing rules.

Need a Deploy Pipeline You Can Trust?

I build Next.js and AI apps on Vercel with CI that tests the live routes, not just the build. Tell me what you ship and how it gets deployed today.

Related Posts