DEV Community

Sakarikos Kleanthis
Sakarikos Kleanthis

Posted on

Fixing "Error 1102: Script startup exceeded CPU time limit" on Cloudflare Workers with Next.js

Fixing "Error 1102: Script startup exceeded CPU time limit" on Cloudflare Workers with Next.js

If you're deploying a Next.js app to Cloudflare Workers via OpenNext and you've just hit this in your logs or a failed deploy:

Error 1102: Script startup exceeded CPU time limit
Enter fullscreen mode Exit fullscreen mode

...you're not alone, and it's not really a bug in your app logic. It's almost always caused by one specific, easy-to-miss pattern in how your app gets bundled. Here's what's actually happening, and a free tool that can point you straight at the cause.

Why this happens

Cloudflare Workers run your code in a V8 isolate. Every time a fresh isolate spins up ("cold start"), it gets a very small CPU budget to finish starting up — parsing and executing the top level of your script — before it's allowed to actually handle the incoming request.

Here's the problem: when OpenNext compiles your Next.js app into a single Cloudflare Worker bundle, a lot of setup code that looks fine in source can end up running at that top level — the module's global scope — instead of inside your actual request handler. Things like:

// This runs on every single cold start, before any request is handled
const db = drizzle(env.DB);
Enter fullscreen mode Exit fullscreen mode

If enough of this kind of work piles up at the top level — a database client being constructed, a large i18n translation object, a big Zod validation schema tree — the cold-start CPU budget gets blown before your handler even runs. Cloudflare kills the isolate and you get Error 1102.

Cloudflare's own docs are direct about the fix: "To reduce startup time, avoid expensive work in global scope. Move initialization logic into your handler." True, but tracking down exactly which import or initialization is the culprit inside a fully bundled, minified worker.js is genuinely tedious — there's no framework tooling that points you at it directly.

The fix, in principle

Defer the expensive part into a lazy getter, so it only runs the first time it's actually needed — not on every cold start:

// Before — runs on every cold start
const db = drizzle(env.DB);

// After — runs once, on first actual use
let _db;
function getDB(env) {
  if (!_db) _db = drizzle(env.DB);
  return _db;
}
Enter fullscreen mode Exit fullscreen mode

Simple in isolation. The hard part is finding every place in a large, real, minified bundle where this pattern is hiding.

A free tool to find it: edge-shake

I built edge-shake, a small CLI that scans your compiled .open-next/worker.js and flags exactly this kind of risky top-level code, with an explanation of why it's risky and a suggested fix.

npx edge-shake .open-next/worker.js
Enter fullscreen mode Exit fullscreen mode

It's read-only — it never modifies your files, just tells you what to look at.

A real example

Here's edge-shake running against a genuinely bundled-and-minified Drizzle ORM client construction — the same kind of output esbuild produces when it bundles a Cloudflare Worker, not a hand-written test case:

$ npx edge-shake fixtures/real-minified-drizzle.js
edge-shake: found 1 risky top-level pattern(s) in fixtures/real-minified-drizzle.js

[MEDIUM] fixtures/real-minified-drizzle.js:14:24929 — orm-client-construction
  Matched: possible Drizzle ORM client (name mangled by bundler, matched via fingerprint)
  Top-level call taking an "env"-shaped argument, in a file that contains Drizzle ORM's
  runtime fingerprint — this MIGHT be a bundled/minified ORM client construction where the
  original constructor name was renamed by the bundler. This is a lower-confidence heuristic
  match (the fingerprint could belong to unrelated code elsewhere in the bundle) — verify
  manually before treating it as confirmed.
  Suggested fix (not applied):
    If this is client construction, wrap it in a lazy getter so it only runs on first use:
    let _client;
    function getClient(env) {
      if (!_client) _client = /* this call */;
      return _client;
    }

Summary: 1 finding(s) (0 high severity). This is a heuristic diagnostic — "risky", not
"will fail". Run with a paid license to auto-fix allowlisted patterns.
Enter fullscreen mode Exit fullscreen mode

Worth calling out: this is flagged MEDIUM, not HIGH — and that's deliberate, not a bug. Minifiers rename identifiers (in this case, drizzle became a single mangled letter), so name-based matching alone silently misses real cases after bundling. edge-shake falls back to checking for string-literal fingerprints that libraries like Drizzle can't avoid shipping (e.g. "drizzle:entityKind") even when everything else is renamed — but that's a file-wide co-occurrence signal, not proof, so it's capped at medium severity and clearly labeled as needing manual verification. I'd rather flag something for you to double-check than silently miss it, or silently claim more confidence than the signal actually supports.

Other patterns it catches: large object/array literals (bloated i18n JSON, static config), large Zod/jose/jsonwebtoken/yup/ajv schema trees, and repeated TextEncoder/TextDecoder construction inside a top-level loop.

Try it

npx edge-shake .open-next/worker.js
Enter fullscreen mode Exit fullscreen mode

Run it as a post-build step, right after opennextjs-cloudflare build and before wrangler deploy. It's free, open source, and read-only.

If you want it fixed automatically

The diagnostic is free and always will be. If you'd rather not hand-fix every flagged pattern yourself, there's an optional paid tier that does the rewrite for you:

npx edge-shake fix .open-next/worker.js --write
Enter fullscreen mode Exit fullscreen mode

It's deliberately conservative — it only touches patterns it can fully verify (currently Drizzle, Prisma, and Kysely client construction), and if it can't confidently trace every reference to a binding, it skips that one and tells you why instead of guessing. You can get a license here if that's useful to you.

If you're hitting Error 1102 right now and the free diagnostic doesn't correctly flag your specific case, I'd genuinely like to know — open an issue with what you're seeing.

Top comments (0)