Why Cold Starts Kill Agent UX
When a user sends a message to an AI agent, they are already waiting for LLM inference — typically 500–2000ms. Adding an 800ms cold start on top of that is catastrophic for perceived performance.
We were on Vercel. Cold starts for our edge functions averaged 800ms on the first request after an idle period. For a platform where latency is the product's core quality signal, this was unacceptable.
The Migration
We moved to Cloudflare Pages via @cloudflare/next-on-pages. The results:
| Metric | Vercel | Cloudflare Pages |
|---|---|---|
| Cold start (p50) | 820ms | 42ms |
| Cold start (p99) | 2,100ms | 180ms |
| Global PoPs | 18 | 300+ |
| Pricing per request | $0.000006 | $0.0000003 |
The 20x cold start improvement and 20x cheaper per-request pricing made the migration economics obvious.
The Gotchas
No Node.js APIs. Cloudflare Workers run the V8 isolate, not Node.js. Anything that imports fs, path, crypto (Node version), or http will fail at build time.
Replace with Web APIs:
crypto.randomUUID() instead of require('crypto').randomUUID()crypto.subtle.digest() instead of createHash()fetch() instead of node-fetch or axiosIn-memory state resets per isolate. Each Cloudflare Worker isolate is independent. An in-memory rate limiter (using a Map) works on a single server but is meaningless across 300 PoPs — each PoP has its own independent Map.
For distributed rate limiting, use Cloudflare KV or the native Rate Limiting product.
WebCrypto API differences. The Web Crypto API is subtly different from Node crypto. In particular, crypto.subtle.digest() returns an ArrayBuffer, not a Buffer. Code that calls .toString('hex') on the result will silently return [object ArrayBuffer].
// Node crypto (WRONG on Cloudflare)
createHash('sha256').update(key).digest('hex')
// Web Crypto (CORRECT everywhere)
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(key))
Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('')
The Runtime Declaration
Every dynamic route in Next.js that should run on Cloudflare Workers must declare:
export const runtime = 'edge'
Without this, Next.js defaults to Node.js serverless functions (which Cloudflare cannot run). Static routes (RSC with no data fetching) do not need this declaration.
Was It Worth It?
Yes. The 20x cold start improvement is immediately visible to users. The global distribution means users in Singapore get the same latency as users in Virginia. The cost reduction funds more compute budget for inference.
The migration takes 2–4 days for a moderately complex Next.js App Router application:
export const runtime = 'edge' to all dynamic routesThe result is a globally distributed, sub-50ms cold start platform that feels alive to users anywhere in the world.