>_C2CZ
Serverless · Self-Hosting · Security · Independent Ops

Cloudflare Error 1102 CPU Time Limit Exceeded: Fix

Your Cloudflare Worker route just started serving Error 1102, and the error page tells you almost nothing: "Worker exceeded resource limits." Error 1102 is Cloudflare's code for one of two limits — CPU time or memory — and in most real incidents it is CPU: your Worker ran synchronous JavaScript for longer than the plan allows, the runtime terminated the isolate, and the visitor got the generic error page instead of your response.

I hit this last month on a catalog search Worker. Locally the handler answered in 40 ms; at the edge the same request died with 1102 on the Free plan's 10 ms CPU budget. The confusing part was that nothing in the code looked slow — a KV read, a JSON parse, a filter loop. The fix was understanding which of those operations actually counts as CPU time, because the answer is not what most people expect.

TL;DR

  • Error 1102 = your Worker exceeded a resource limit — CPU time or the 128 MB memory ceiling. The public page says only "Worker exceeded resource limits"; the dashboard tells you which one (see below).
  • CPU time ≠ wall time. Only synchronous JavaScript execution counts — loops, JSON parsing, sorting, string building. Time spent waiting on fetch(), KV/R2 reads, or the network does not count.
  • Free plan: 10 ms of CPU per HTTP request. Paid plan: 30 s default, configurable up to 5 minutes. Memory: 128 MB per isolate on both plans.
  • Diagnose first: Workers & Pages → your Worker → Metrics → Errors → Invocation Statuses shows "Exceeded CPU Time Limits" or "Exceeded Memory"; Logpush/analytics record the outcome as exceededCpu or exceededMemory.
  • Fix in order: profile, then cut per-request CPU (cache parsed data, index once per isolate), then raise cpu_ms on Paid, then offload heavy work to Cron Triggers or Queues. Do not confuse 1102 with the deploy-time validation error 10021 ("Script startup exceeded CPU time limit") — that one means your module top-level scope is too heavy and Cloudflare rejects the upload before any traffic is served.

What Error 1102 actually means

Per the Cloudflare support documentation, Error 1102 indicates that a Worker has exceeded its CPU time limit or its memory limit. The runtime returns the code to the client with the deliberately vague message Worker exceeded resource limits — Cloudflare does not leak your internal limits or code details to the public, so the Ray ID on the page and your own metrics are the only way to know which resource blew.

That makes 1102 different from the errors we covered in the Error 1101 write-up: 1101 means your code threw an unhandled JavaScript exception, while 1102 means your code ran correctly but used too much of a fixed budget. It is also distinct from Cloudflare 524, where the origin never answered in time and the Worker may have done nothing wrong. 1102 is a budget problem, and budgets are measurable.

Plan limits: the budget numbers

These are the current numbers from the Workers platform limits documentation (verified 2026-09-02). CPU time is measured per isolate per request; memory is per isolate, which may serve several requests concurrently.

ResourceWorkers FreeWorkers Paid
CPU time per HTTP request10 ms5 min (default 30 s)
CPU time per Cron Trigger10 ms30 s (interval under 1 h) / 15 min (interval 1 h or more)
Memory per isolate128 MB128 MB
Worker startup time (CPU)~1 s~1 s
Subrequests per invocation5010,000

⚠️ The 10 ms Free-plan number surprises everyone, because 10 ms of wall time is nothing — but it is 10 ms of CPU, and modern V8 executes a lot of JavaScript in 10 ms. A single multi-megabyte JSON.parse() can eat the whole budget by itself. Isolates also get a little built-in flexibility for rare overruns; it is only when a Worker consistently runs over that execution is terminated at the configured limit.

Diagnose before you touch code

Do not guess whether it is CPU or memory. The dashboard names the resource. Go to Workers & Pages → your Worker → MetricsErrorsInvocation Statuses and look at the window where the error appeared:

  • "Exceeded CPU Time Limits" — CPU budget. Optimize synchronous work, raise cpu_ms on Paid, or offload.
  • "Exceeded Memory" — 128 MB isolate ceiling. Stream bodies instead of buffering, stop caching large objects in module scope, watch for accumulating arrays/strings.

If you ship analytics to Logpush, the invocation outcome field is the machine-readable version: exceededCpu for CPU limit, exceededMemory for memory. Filter on those two outcomes and you get the failing requests with their timestamps and URLs — far more precise than watching the error page appear.

💡 For local reproduction, wrangler dev gives you a DevTools profiler: press D in the terminal, open the Profiler tab, record, then send the failing request. The profile shows CPU time per function, which turns "my Worker is slow" into "this function burns 8 ms of the 10 ms budget."

Fix 1 — cut the CPU you burn per request

Most 1102 incidents are not one giant computation; they are small synchronous costs paid on every request. Here is the pattern that cost me the incident: a catalog Worker that read a large JSON document and parsed it on every single request. The KV read is I/O and does not count as CPU — but the parse does, and on the Free plan it alone can exceed the budget:

// What NOT to do: parse a large KV-backed JSON document and scan it on
// every request. The KV read is I/O (not CPU), but JSON.parse() of a
// multi-megabyte document is synchronous CPU. On the Free plan the whole
// request must fit in 10 ms of CPU — this one does not.
export default {
  async fetch(request, env) {
    const q = (new URL(request.url).searchParams.get("q") ?? "").toLowerCase();
    const raw = await env.PRODUCTS.get("catalog.json"); // I/O: not CPU
    if (raw === null) {
      return Response.json({ error: "catalog_missing" }, { status: 503 });
    }
    const rows = JSON.parse(raw); // synchronous CPU: the bill

    const matches = [];
    for (const row of rows) {
      if (row.name.toLowerCase().includes(q)) {
        matches.push(row);
        if (matches.length === 20) break;
      }
    }
    return Response.json({ matches: matches.map((r) => r.id) });
  },
};

Rough numbers matter here: V8 parses JSON at tens of megabytes per second, so a 2–5 MB document is tens to hundreds of milliseconds of CPU — comfortably over 10 ms, and it eats into the 30 s Paid default too if the document is large and traffic is steady. The parse is pure waste when the data has not changed.

The fix is to parse once per isolate and reuse the result, with the parse memoized behind a promise so concurrent first requests do not each pay it:

// Fixed: parse once per isolate and reuse. Cold starts still pay the
// parse once — pair this with a warm-up Cron Trigger (Fix 3) or a raised
// cpu_ms on the Paid plan (Fix 2) if the document is very large.
let catalogPromise = null;

async function loadCatalog(env) {
  const raw = await env.PRODUCTS.get("catalog.json");
  if (raw === null) throw new Error("catalog missing from KV");
  return JSON.parse(raw); // paid once per isolate, not per request
}

function getCatalog(env) {
  catalogPromise ??= loadCatalog(env);
  return catalogPromise;
}

export default {
  async fetch(request, env) {
    const q = (new URL(request.url).searchParams.get("q") ?? "").toLowerCase();
    try {
      const rows = await getCatalog(env);
      const matches = [];
      for (const row of rows) {
        if (row.name.toLowerCase().includes(q)) {
          matches.push(row);
          if (matches.length === 20) break;
        }
      }
      return Response.json({ matches: matches.map((r) => r.id) }, {
        headers: { "X-Content-Type-Options": "nosniff" },
      });
    } catch (err) {
      console.error("catalog lookup failed", err.message);
      return Response.json({ error: "catalog_unavailable" }, {
        status: 503,
        headers: { "Cache-Control": "no-store" },
      });
    }
  },
};

Keep the same eye on every per-request cost: avoid re-parsing configuration, re-building lookup tables, or re-sorting static data inside the handler. If you build an index over the data (a Map keyed by token, for example), build it once next to the cached parse, not inside the loop that serves requests. Cloudflare's own guidance for reducing CPU usage is the same list: fewer loop iterations, cheaper JSON parsing, caching computed values, and breaking large operations into smaller chunks.

Fix 2 — raise the CPU budget on Workers Paid

If the computation is genuinely necessary — image processing, schema validation over a large document, a real search index build — optimization has a floor. On the Workers Paid plan you can raise the CPU limit from the 30 s default up to 5 minutes (300,000 ms) in wrangler.toml:

# wrangler.toml — Workers Paid only.
# Free plan CPU is fixed at 10 ms per HTTP request and cannot be raised.
[limits]
cpu_ms = 300_000  # default is 30_000; maximum is 300_000 (5 minutes)

The equivalent dashboard path is Workers & Pages → your Worker → Settings → adjust the CPU time limit. Redeploy after changing wrangler.toml for the new limit to take effect.

⚠️ Raising the budget is not a free pass. A request that burns 4 minutes of CPU ties up an isolate and costs real money at scale — and CPU-bound Workers under heavy load can surface as HTTP 503s as well as 1102, per Cloudflare's related-errors documentation. Treat the raised limit as headroom for legitimate heavy work, not as permission to skip Fix 1.

Fix 3 — take the work off the request path

Some work does not belong in a request at all. The cleanest 1102 fixes move computation out of the request handler entirely:

  • Precompute with a Cron Trigger. A scheduled Worker builds the index or summary once, stores the result in KV or R2, and the request handler only reads the finished artifact. This is the pattern I ended up with for the catalog: a nightly job writes catalog.index.json; requests just fetch and serve it. Cron Triggers get their own CPU budget per invocation (30 s to 15 min on Paid), separate from HTTP requests.
  • Defer with Queues. For expensive post-request processing — resizing an upload, generating a report — return a 202 immediately and enqueue the job. Queue consumers run as separate invocations with a 15-minute wall time, so the slow work never touches the request's CPU budget.
  • Do not buffer what you can stream. If the 1102 is the memory variant, the fix is streaming: process request and response bodies through TransformStream instead of await request.text() or await response.json() on multi-megabyte payloads, and never accumulate rows into a growing array when a pass-through stream works.

💡 The same "what counts as CPU" logic applies to backend calls made from a Worker. An HTTP fetch to Turso or any libSQL endpoint waits on the network — that wait does not consume CPU, so slow external services are rarely the direct cause of 1102. What consumes CPU is parsing and processing the response afterwards.

Not 1102: "Script startup exceeded CPU time limit" (error 10021)

Before you chase a runtime budget, rule out the deploy-time cousin. When the top-level scope of your Worker — module code that runs before the first request — takes more than the 1 s startup CPU limit, Cloudflare rejects the deployment with validation error 10021: "Script startup exceeded CPU time limit." This is not 1102: 1102 happens at runtime on requests that are already serving, while 10021 happens at upload time, before the Worker ever handles traffic. There is a matching memory case, "Script startup exceeded memory limit", when top-level scope allocates more than the 128 MB limit.

The usual culprits are heavy module-scope work: building a large index at import time, constructing a database client eagerly, parsing a giant config or i18n bundle at the top of the file. If you deploy a bundled framework worker (Next.js on OpenNext is a common example), the bundle itself can push startup over the edge. Wrangler reports startup_time_ms when you deploy, and on a 10021 rejection it generates a CPU profile you can import into Chrome DevTools or VS Code — run npx wrangler check startup for the deep dive. This is what the bad version of my catalog cache looked like before I moved it:

// What NOT to do: eager module-scope work. On deploy, Cloudflare loads
// the top-level scope inside the ~1 s startup CPU budget. Large datasets
// here make the upload fail with:
//   Error: Script startup exceeded CPU time limit. [code: 10021]
let byToken = null;

function buildIndex() {
  // Stand-in for real heavy init: a search index build, schema
  // validation, or i18n expansion over a large loaded dataset.
  const rows = globalThis.__CATALOG__; // populated from KV in a real worker
  byToken = new Map();
  for (const row of rows) {
    for (const token of row.name.toLowerCase().split(" ")) {
      if (!byToken.has(token)) byToken.set(token, []);
      byToken.get(token).push(row.id);
    }
  }
}

buildIndex(); // runs at module evaluation time — cold start pays it

The fix is the same lazy, memoized pattern from Fix 1, applied at module scope: keep the top level free of side effects and do the expensive build on first use. The docs guidance is blunt — avoid expensive work in global scope and move initialization into your handler:

// Fixed: defer the build to the first request and memoize it per isolate.
let indexPromise = null;

function getIndex() {
  indexPromise ??= Promise.resolve().then(buildIndex);
  return indexPromise;
}

export default {
  async fetch(request, env) {
    try {
      const byToken = await getIndex(); // first request builds; later ones reuse
      const q = (new URL(request.url).searchParams.get("q") ?? "").toLowerCase();
      const ids = q ? (byToken.get(q) ?? []) : [];
      return Response.json({ count: ids.length, ids: ids.slice(0, 50) }, {
        headers: { "X-Content-Type-Options": "nosniff" },
      });
    } catch (err) {
      return Response.json({ error: "index_unavailable" }, { status: 503 });
    }
  },
};

Note the trade-off: deferring moves the CPU cost from the startup budget to the first request's budget. If the build genuinely takes longer than your plan's per-request CPU limit, deferral alone just relocates the 1102 — combine it with the Cron Trigger precompute pattern so requests never perform the build at all.

Verify the fix

  1. Reproduce the failure first. Hit the route that produced 1102 (load-test it if the error only appears under traffic) and confirm the dashboard shows "Exceeded CPU Time Limits" for the window.
  2. Deploy the fix with npx wrangler deploy after editing wrangler.toml or your Worker code.
  3. Repeat the same request. Expect your response, not the 1102 page. For the catalog example above: a 200 with JSON on the happy path, a 503 with a clean body when KV is missing.
  4. Watch Invocation Statuses for the next several minutes: no new "Exceeded CPU Time Limits" (or "Exceeded Memory") entries in the window. If you use Logpush, confirm the exceededCpu / exceededMemory outcomes stop appearing.
  5. Re-check at cold start: after an idle period (or after a redeploy), request the route again — this is when the first-request parse and index-build cost shows up. Startup CPU itself is checked at deploy time: wrangler prints startup_time_ms, and a 10021 rejection tells you the top-level scope is too heavy before any traffic is at risk.

When 1102 is not your code

Before rewriting a working handler, rule out the surrounding cases. A 503 at the same time as 1102s can be the same resource limit surfacing through the edge, per Cloudflare's related-errors documentation — so check both error codes in your metrics. If requests fail before your code even runs, or across many Workers at once, check the Cloudflare status page for a Workers runtime incident before optimizing anything. And if the pattern is a slow origin rather than a hot Worker, the timeout path is Error 524, covered separately here.

Error 1102 is a budget problem with a measurable fix. Confirm which resource exceeded, cut the per-request CPU, raise the limit only where the work is legitimately heavy, and move what does not belong in a request to a Cron Trigger or Queue. When the dashboard stops showing "Exceeded CPU Time Limits," the fix is done — and the next 1102 you see will be a diagnosis, not a mystery.

C2CZ

Hyper-specific engineering guides: Cloudflare Workers, Turso, self-hosting, network security, offensive security, independent operations.