Johin published this on the ToolBox blog on 3 March 2026. It is a first-party post about their own Next.js app. There is no dollar figure. “Dropped significantly” and the millisecond claims are his.
ToolBox had been on Vercel from the first commit. Push to GitHub, a deployment, preview URLs, and a Next.js integration that Vercel owns. The Vercel → Cloudflare Pages playbook is builds, functions, env, and DNS. His post is what static export did to a site that still had 20 API routes.
Cost is the reason he gives. Vercel priced the server-side work. API routes, server-rendered pages, and image optimization all counted. A tool site with 139-plus pages and 20 API routes made that climb. Cloudflare Pages serves static files from the CDN without a per-request asset charge, and Pages Functions run on Workers. KV covered rate limits. He says the infrastructure bill fell. He does not say by how much.
Two architectures in one write-up
The before picture is an ordinary Next.js app. Some pages server-rendered, some static. API routes as Node serverless functions. Vercel’s image optimizer. Env vars in the Vercel dashboard. Deploys from master.
The after picture, in the first half of the post, is output: "export" in next.config.ts. HTML, CSS, and JS land in out/. API routes move to functions/, as Pages Functions with onRequestPost and onRequestOptions. getServerSideProps had to go. He says ToolBox had few of those, because the tools run in the browser, but the blog and some metadata pages needed a rewrite. app/api/ still exists for local dev and does nothing in production. Dynamic routes have to be known at build time. The 139 tool pages already had generateStaticParams. Blog posts needed every slug listed.
Later he says the migration uses @opennextjs/cloudflare at ^1.17.1, as a dev dependency, and that the adapter writes .open-next/ with compiled server functions and asset mappings. OpenNext is how you keep a Next server on Workers. output: "export" is how you stop having a Next server. The post never says which one is in production, or whether one replaced the other. I would not merge them into a single deploy diagram. The build script he does print, scripts/cf-build.mjs, runs next build for the static export and copies functions/. pages:build and cf:deploy both call that script. Cloudflare Pages runs it on a push to master.
The first deploy missed the Worker limit by about 3.3 MB
The Worker bundle passed 25 MB. He cut about 3.3 MB.
Sentry left. He says the client SDK is around 70 KB gzipped, and the server SDK inside API routes was the real weight. A small handler now posts to /api/errors/report, which logs with Cloudflare’s own logging. /api/errors/list is how they read recent errors. If volume grows, he would add a tracker again on the server, not as a client SDK in the bundle.
@vercel/og left. It pulls in Satori and a WASM text renderer, and he says that was over 2 MB. Open Graph images moved to a simpler Pages Function. He does not show that function.
Blog bodies had been imported into the JavaScript bundle. They moved to Markdown under content/blog/, read at build time for pages and on request for the API.
next/image optimization does not run on Cloudflare. He says ToolBox does not depend on dynamic images, so static files were enough.
What each API route had to relearn
The signature change is the whole port. A Next POST(request: NextRequest) became onRequestPost({ request, env }), returning Response.json instead of NextResponse.json. Env vars are env.PADDLE_API_KEY, not process.env.PADDLE_API_KEY. NEXT_PUBLIC_ values still inline at build time. There is no Node fs, path, net, or http. Web Crypto replaces crypto.createHash. It is async. MD5 is missing from Web Crypto, and the hash endpoint still needs it for checksums, so they ship about 120 lines of RFC 1321 in JavaScript.
Vercel attached CORS for same-project API routes. Pages Functions do not. Every endpoint grew an OPTIONS handler and an Access-Control-Allow-Origin: * header set. He calls that tedious.
Workers, in his comparison, have a 128 MB memory cap and 30 seconds of CPU on the paid plan. He puts Vercel functions at up to 1024 MB and 5 minutes. ToolBox endpoints are short transforms, so the cap was fine. The diff endpoint refuses strings over 100 KB because an LCS diff is quadratic and could blow the CPU budget.
File routes he lists: functions/api/v1/json-format.js, functions/api/v1/hash.js, functions/api/dns-lookup.js, functions/api/paddle/verify.js, functions/api/errors/report.js.
Rate limits moved from process memory to KV
On Vercel, subscription checks kept device state in memory inside the function. A new isolate did not see the last one. On Cloudflare, verification reads CF-Connecting-IP, hashes it with SHA-256 and a salt, and stores up to three hashes per subscription id in KV for 30 days. A fourth device gets a 429. The binding is PRO_RATE_LIMIT. The key looks like rl:${subscriptionId}. If KV throws, the check fails open and allows the request. expirationTtl is 30 days, so there is no cleanup job.
He also says KV can take up to 60 seconds to show a write at every edge. Two verifications in that window, on two edges, can both see an empty list. He says verification happens once when you load the site, so he is not treating that race as a real abuse path.
Country for Pro pricing comes from CF-IPCountry. On Vercel that was a separate geolocation call.
Cold starts, previews, and a DNS cut with the old host still up
His performance numbers, labeled as his: Vercel cold starts of 200 to 500 ms while a Node process spins up, Workers under 5 ms, and sub-50 ms for compute-bound endpoints after that. Static assets move to a network he counts as 300-plus data centers. Builds got slightly slower because all 139-plus tool pages render up front. He measures that slowdown in seconds.
HTML cache is public, max-age=0, s-maxage=3600, stale-while-revalidate=86400. The browser revalidates. The edge may hold the page for an hour and serve stale for a day. Fingerprinted assets are public, max-age=31536000, immutable. Security headers (nosniff, SAMEORIGIN, an old X-XSS-Protection, referrer policy, and a locked-down permissions policy) are set in next.config.ts and, he says, applied at the CDN for both static files and functions.
They lost Vercel’s analytics dashboard and a tighter GitHub preview integration. Cloudflare still gives a preview URL per branch, and the dashboard can roll back to an older deployment. wrangler pages dev is how they found V8 mismatches before production.
Split View was the layout bug. Two tools load in iframes. The static HTML included the site header inside each pane. A query param named embed hides header, footer, and nav in the client.
Cutover was the old host left running. They deployed to Cloudflare, checked it, then moved DNS. He says downtime was zero. The focused work was about a week, mostly API rewrites and export debugging. His closing limit is the useful one. If the app depends on server rendering, Server Actions, or ISR, this trade is different, because those sit close to Vercel’s platform. ToolBox is static pages plus short request handlers, which is the shape this post actually ports.