We run Hizmetgo, a Turkish local-services marketplace (think Thumbtack for Türkiye). The site is programmatic-SEO heavy: ~1M+ Next.js ISR pages across 81 provinces, 970+ districts and thousands of service categories. Here's the infrastructure story of how we left Vercel, moved to a single Hetzner VPS, hit a disk-full crash at 3am, and fixed it with an in-memory cache handler.
Why we left Vercel
Vercel was excellent for shipping fast, but at our page volume the ISR invocation + bandwidth bill climbed past what a bootstrapped startup wanted to pay (~$150/mo trending up). We wanted predictable cost and control, so we moved to a Hetzner VPS with Coolify (an open-source, self-hosted Vercel/Heroku alternative) for git-push auto-deploys, with Cloudflare in front for caching and TLS.
The migration itself was smooth: Coolify builds the Next.js app (Turbopack), a webhook auto-deploys on every push to main, and Cloudflare's "Cache Everything" absorbs the bot storm on our SEO pages.
The 3am disk-full crash
A few days in, the container started crashing. df -h showed the disk at 100%. The culprit was subtle: Next.js ISR writes each rendered page to the filesystem cache — and with ~1M crawlable pages, Googlebot plus our own crawl was generating ~145GB/day of cache files inside the running container's writable layer. docker system prune couldn't reclaim it (the files belong to the running container, not a stopped one), so the disk just filled until the box fell over.
The fix: an in-memory cache handler
Next.js lets you swap the ISR cache backend via cacheHandler in next.config. We pointed it at a small in-memory handler so the cache lives in bounded RAM instead of unbounded disk:
// cache-handler.js
const cache = new Map(); // swap for lru-cache in prod to bound memory
module.exports = class CacheHandler {
async get(key) { return cache.get(key) ?? null; }
async set(key, data, ctx) {
cache.set(key, { value: data, lastModified: Date.now(), tags: ctx?.tags });
}
async revalidateTag() { /* clear by tag */ }
};
Enter fullscreen mode Exit fullscreen mode
// next.config.js
module.exports = {
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0, // disable the default in-process cache; we manage it
};
Enter fullscreen mode Exit fullscreen mode
Plus a COPY cache-handler.js line in the Dockerfile so it ships with the standalone build. Result: even if all 1M pages get crawled in a day, the disk never fills — the cache is capped in memory, and Cloudflare handles the long-tail hit rate at the edge.
Other things that helped
- Cloudflare edge caching on the SEO path so cold long-tail TTFB stays sane.
-
Prisma
connection_limittuned down for a single VPS (vs serverless fan-out) so we don't exhaust the Postgres (Supabase) pool. - A dedicated read path for SEO pages so a crawl wave can't starve auth.
If you're self-hosting a large ISR Next.js site, watch the cache write path before it watches you. Our data pages (e.g. the Türkiye service price index) are all served this way now — 1M pages, one modest box.
Stack: Next.js 16 · Prisma · Supabase (Postgres) · Expo (mobile) · Cloudflare · Hetzner + Coolify · Turbopack.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.