Nuxt Content v3 queryCollection() Returns Empty on Vercel (But Works Locally)

A production-only bug where queryCollection("blog").all() silently returns an empty array on Vercel while the exact same build works fine locally — caused by @nuxt/content 3.0.0 predating the Vercel /tmp SQLite fix.

Nuxt Content v3 queryCollection() Returns Empty on Vercel (But Works Locally)

A Nuxt 3 + Nuxt Content v3 blog suddenly started rendering a completely empty post list in production. No error, no crash — the page just rendered with zero posts. Locally, running npm run build and serving that exact same build worked perfectly: every post showed up.

That gap — identical code, different result depending on where it runs — is the signature of an environment-specific bug, not a logic bug. Here's how it was tracked down.


The symptom

<script setup>
const { data: posts } = await useAsyncData("blog-post-list", async () => {
  const result = await queryCollection("blog").all();
  return result;
}, { default: () => [] });
</script>
  • Locally (npm run build + node .output/server/index.mjs): posts comes back with every post. Works.
  • On Vercel, hitting the live URL: posts comes back as []. Every time. No error in the response, no warning in the logs — just an empty array.

Confirmed it wasn't a caching artifact first: the response headers on Vercel showed x-vercel-cache: MISS and age: 0 — a genuinely fresh server execution, still returning nothing.


Ruling things out

Since the exact same build artifact behaved differently by environment, the cause had to be something about where it ran, not the page code:

  • Not a missing content.config.ts — it was present and correctly configured.
  • Not a code bug in the page — the identical build rendered all posts correctly when run locally.
  • Not a stale CDN cache — confirmed via response headers (MISS, age: 0).

That narrows it down to the one thing that genuinely differs between "run locally" and "run inside a Vercel serverless function": filesystem access.


Root cause: @nuxt/content v3's SQLite database needs /tmp on Vercel — and 3.0.0 didn't know that yet

@nuxt/content v3 indexes all your markdown into a SQLite database at build time, read via better-sqlite3 (a native module) at runtime. On Vercel, the deployed function's filesystem is read-only everywhere except /tmp. If the content module tries to read or write its SQLite file somewhere outside /tmp, that access silently fails in a way that doesn't throw — queryCollection() just comes back with nothing, instead of erroring.

The fix for this exact scenario — making the module use /tmp on Vercel — shipped in @nuxt/content v3.1.1 (nuxt/content#3108). The project in question was pinned to @nuxt/content: "^3.0.0" — the very first v3 release, published before that fix existed.

That's the whole bug: a brand-new Nitro/Nuxt project scaffold had pinned the first-ever v3 release of the content module, and that specific version never got the Vercel filesystem fix. Locally, there's no /tmp-only restriction, so the broken path silently worked anyway — which is exactly why this kind of bug survives so long undetected: it only manifests in the one environment nobody manually tests against on every change.


The fix

Upgrade past the fix, plus whatever else comes along for the ride:

- "@nuxt/content": "^3.0.0",
+ "@nuxt/content": "^3.16.1",
- "nuxt": "^3.15.2",
+ "nuxt": "^3.21.11",
- "@nuxtjs/sitemap": "^7.2.9",
+ "@nuxtjs/sitemap": "^8.6.1",

A few things fall out of bumping @nuxt/content that far forward, worth knowing before you hit them:

  • @nuxt/content@3.16.1 requires nuxt >=3.19.0 (or ^4.1.0) — the module disables itself with a warning if your Nuxt version is older, rather than failing loudly.
  • Newer @nuxt/content no longer bundles better-sqlite3 — it now expects it as an explicit dependency. nuxt prepare fails with Nuxt Content requires better-sqlite3 module to operate until you npm install better-sqlite3.
  • @nuxtjs/sitemap's older content integration (asSitemapCollection) imports queryCollectionWithEvent from @nuxt/content's runtime — a function that newer content versions renamed/removed. The build fails at the Rollup stage with "queryCollectionWithEvent" is not exported until @nuxtjs/sitemap is bumped to a version built against the newer content API.

Verifying the fix

Since this bug only showed up in the deployed environment, "it builds locally" isn't enough evidence. After upgrading:

  1. npm run build, then run the actual build output locally (node .output/server/index.mjs) and confirm the post count matches your content directory.
  2. Deploy, then check the live response headers for x-vercel-cache: MISS / fresh age to make sure you're not looking at a cached (and still-empty) response.
  3. Check /sitemap.xml on the live deployment — it should list every post, not just static routes.

Takeaway

If queryCollection() (or any SQLite-file-backed Nuxt module) works locally but comes back empty only on Vercel with zero errors, check your filesystem assumptions before your logic — and check whether you're pinned to a version of the module that predates its own Vercel-specific fix. "Works on my machine" is a filesystem-permissions story as often as it's a cache one.