Vercel & Remix
Vercel's free hobby plan looks convenient: push to deploy, global edge, previews. After a week of testing a Remix app, platform resource ceilings (especially file handles) became the actual bottleneck.
Persistent EMFILE errors (too many open files) appeared only in the Vercel environment, never locally:
Error: EMFILE: too many open files, open '/var/task/node_modules/nanostores/index.js'
at Object.openSync (node:fs:562:18)
at readFileSync (node:fs:446:35)
at getSourceSync (node:internal/modules/esm/load:66:14)
at getSource (node:internal/modules/esm/translators:68:10)
at createCJSModuleWrap (node:internal/modules/esm/translators:185:32)
at ModuleLoader.commonjsStrategy (node:internal/modules/esm/translators:275:10)
at #translate (node:internal/modules/esm/loader:534:12)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:581:27)
Node.js process exited with exit status: 1.
And Vercel helpfully adds:
The logs above can help with debugging the issue.
In practice that note doesn't help; you don't get visibility into the underlying per-process file descriptor limit. And those error messages are plain unhelpful.
This isn't just a "me" problem. After digging into community discussions, it's clear that Remix users are particularly susceptible to these issues on Vercel. The platform's serverless runtime is more restrictive than local development environments, making these file handle limitations show up exclusively during deployment.
What the Remix Community Is Saying
The evidence is mounting across multiple platforms:
Reddit threads: recurring EMFILE only on deploy, often with lucide-react / other icon sets.
GitHub issues: Remix + Vite + large icon libraries (phosphor, heroicons, lucide) trigger failures; replacing barrel imports with direct paths reduces frequency.
Consensus: platform constraint. Local dev rarely reproduces. Import pattern influences open-file churn.
The community has developed various workarounds:
- Optimizing build-time concurrency
- Refactoring dynamic imports, especially from icon libraries
- Updating dependencies and build tooling
- Switching import patterns from barrel to direct imports
These are mitigations, not root fixes; they trade code clarity for working within an unknown cap.
The Routing Convention Collision
Beyond the file handle limits, Vercel's filesystem-based routing creates another friction point with Remix conventions. Remix uses parentheses in filenames for layout routes—like app/routes/(dashboard).profile.tsx
to create nested layouts without affecting the URL structure.
Vercel's routing system treats parentheses as special characters for dynamic segments and grouping. When you deploy a Remix app with parenthesized route files, Vercel's build process throws errors messages until you stop handing them files with parentheses which ends up breaking your intended routing structure.
Why Vercel needs this convention: Vercel's serverless functions are generated from your filesystem structure. Each route file becomes a separate serverless function endpoint. The platform needs a predictable way to parse filenames into URL patterns, so it reserves certain characters ([]
for dynamic segments, ()
for route groups) for its own routing logic. Remix's parentheses serve a completely different purpose—they're for layout composition, not URL generation. This creates a fundamental impedance mismatch between how Remix thinks about routes and how Vercel processes them.
The workaround tax: You end up refactoring your route organization to avoid parentheses, often losing the clean layout composition that makes Remix routing elegant. Instead of (dashboard)
layouts, you're forced into nested folder structures or other patterns that don't map as cleanly to your component hierarchy.
So what next?
Maybe Fly.io. Stay tuned.