Your URLs Are Filenames Now
One dirty database field took down a static build, exposing a limit every file-per-route generator inherits.
A Russian apartment-listing site, podbor-minuta.ru, prerenders about 600 pages of Moscow listings with Vike, building URLs like /novostroyki/rayon/nagatino/2-komnatnye straight from database fields: city, district, room count. One day the build died with ENAMETOOLONG. A scraper had stuffed a district column with roughly 295 characters of listing description — prices, "finishing," the works — and the URL generator dutifully turned that sentence into a path segment. The filesystem, which caps each path component at 255 bytes, refused to create the directory.
It's a small war story from a small site, and it would be easy to file under "validate your inputs, obviously." That's the wrong lesson, or at least an incomplete one. The interesting part is why this particular class of garbage data crashes this particular class of architecture: file-per-route static generation maps your URL space directly onto the filesystem namespace. The moment you do that, every URL inherits constraints most web developers have never had to think about — and your database becomes able to crash your build.
The limit is bytes, and Unicode halves it
On ext4, XFS, Btrfs, and APFS, a single path component — one directory or file name — maxes out at 255 bytes, not characters. Linux additionally caps a full path at 4,096 bytes. When Node's fs.mkdir or fs.writeFile hits either wall, the syscall bubbles up as ENAMETOOLONG, one of the terser errors in the POSIX catalog.
The byte-vs-character distinction is what makes this bite non-English sites first. Cyrillic characters are two bytes each in UTF-8, so a Russian-language slug hits the ceiling at about 127 characters — half the headroom an ASCII slug gets. CJK characters, at three bytes each, cut it to roughly 85. That 295-character district "name" was likely close to 590 bytes on disk. A validation rule written as length <= 255 in JavaScript, where .length counts UTF-16 code units, would have waved it through. You need Buffer.byteLength(slug, 'utf8'), and almost nobody writes that check until a build has already exploded.
If this sounds like a fringe concern, remember that an entire ecosystem redesigned itself around a path limit once before: npm flattened its nested node_modules layout in version 3 largely because deep dependency trees blew past Windows' legacy 260-character MAX_PATH. Filesystem limits are obscure right up until they're load-bearing.
Not one team's bug
The same crash has been filed against every major file-per-route generator. Gatsby has had an open issue about ENAMETOOLONG on long slugs since 2018 (#4125), plus a separate one for its internal cache filenames. Next.js users hit it during next build on long dynamic segments — with the telltale detail that everything works fine in dev.
That asymmetry is the actual trap. Dev servers resolve routes in memory; nothing touches the disk per-URL, so a 600-byte slug renders happily on localhost. Only static export — or ISR writing its cache — materializes URLs as directories. Your route layer effectively has two implementations with different validation semantics, and the stricter one only runs in CI or production. Any invariant that's enforced by the filesystem rather than by your code is an invariant you'll discover at the worst possible time.
Put the fence at the write boundary
The podbor-minuta fix was a 120-character cap in the URL generator plus filtering suspicious values when reading from the database — rejecting anything that looks like a sentence full of commercial keywords. Reasonable, and the 120-character budget is honest engineering: real district and metro names are far shorter, and it leaves byte-headroom for Cyrillic.
But both fixes sit on the read path, which means the dirty rows are still in the table, waiting for the next consumer that doesn't know about the filter. When the source is a scraper — untrusted input by definition — the constraint belongs where the data lands:
ALTER TABLE districts
ADD CONSTRAINT district_name_sane
CHECK (char_length(name) <= 120 AND octet_length(name) <= 240);
A Postgres CHECK constraint turns "our build crashed and we grepped for the bad row" into "the scraper's insert failed last Tuesday and logged exactly which listing was malformed." octet_length does the byte-level check the filesystem will eventually do anyway, just years earlier and with a better error message. Then treat slug generation as parsing, not sanitizing: allowlist [a-z0-9-] (or your transliteration of it), reject rather than truncate anything that doesn't fit the shape of a real district name, and keep one shared slugifier so the rule can't drift between the scraper, the sitemap, and the page generator.
The build crash was the system working
Here's the contrarian read: ENAMETOOLONG was the good outcome. The same dirty field in a server-rendered app wouldn't have crashed anything — it would have quietly minted a live page at a 590-byte URL, gotten it into the sitemap, and let Google index a listing whose "district" is a paragraph about finishing options. Static generation's core promise is moving failures from request time to build time, and that promise held: the whole class of bug became un-shippable.
The smell isn't that the build failed. It's that the filesystem was the only validator in the stack — a five-layer pipeline of scraper, database, ORM, URL generator, and SSG, where the first four passed garbage along and the kernel finally said no. Generators could soften the landing (Gatsby maintainers have floated truncate-and-warn for years; nobody's shipped it as a default), but the durable fix is cheaper than any framework feature: one CHECK constraint and one byte-aware slug parser. If your routes are built from data you don't control, you're one scraped sentence away from learning what NAME_MAX is. Better to learn it from a failed insert than a red deploy.
Sources & further reading
- ENAMETOOLONG: how one dirty database field crashed our static build — dev.to
- ENAMETOOLONG: name too long for long headers / slugs — github.com
- ENAMETOOLONG on long slug — github.com
Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.
Discussion 0
No comments yet
Be the first to weigh in.