Skip to content
Image KB Reducer
Esc
↑↓navigate↵open⌘Jpreview
On this page

Limits

Byte, pixel, and encode-attempt ceilings — and what actually bounds a running job.

This page is generated from — and kept in sync with, by a CI test — this API’s own wrangler.jsonc vars. If this page and that file ever disagree, the file is right and this page has a bug.

Supported formats

file must be JPEG, PNG, or WebP. HEIC and AVIF are refused with 415 unsupported-format, before any decode is attempted — this is the single most likely trap for a new caller, since a photo straight off an iPhone is HEIC by default.

This isn’t a missing feature waiting on a future release; it’s a real platform ceiling. HEIC decode needs libheif-js, which compiles its wasm module at runtime with new WebAssembly.Module(bytes) — and Cloudflare Workers disallow dynamic wasm compilation from bytes at request time, the same restriction that blocks eval. This API’s wasm codecs are all statically imported .wasm files, compiled by wrangler at deploy time instead; HEIC’s decoder is the one codec that structurally can’t work that way here. AVIF’s reason is simpler: this API bundles no AVIF decoder at all (the wasm AVIF decoder is roughly 8.4 MB and multi-second per image, and there’s no canvas fallback available in a Worker).

HEIC decodes natively in the CLI and the web app, so either one will convert a HEIC source for you first. AVIF is narrower: the CLI runs the same wasm backend this API does and bundles no AVIF codec either (it exits 20 unsupported-format), so for an AVIF source the web app — which decodes through the browser’s own image pipeline, wherever that browser supports AVIF — or any other converter is the way in.

The numbers

Var Value What it bounds
MAX_INPUT_BYTES 26,214,400 bytes (25 MiB) the upload — refused as 413 input-too-large, from the Content-Length header alone when present, before the body is read
MAX_MEGAPIXELS 20 MP decoded (or requested output) pixel dimensions — refused as 422 dimension-exceeded, checked from the file header before a full decode
MAX_ENCODES 8 attempts how many encode probes the target-size search can make on one request — the search reports target-unreachable / BUDGET_EXHAUSTED rather than running indefinitely
WALL_CLOCK_MS 25,000 ms see Why the wall clock isn’t the real bound below — this number is configured but not what you should rely on

Sizes are 1024-based throughout this API, matching the rest of the product: target=50kb means 51,200 bytes, and the 25 MiB input ceiling above is 25 * 1024 * 1024 bytes, not 25,000,000.

The 20 MP ceiling is measured, not estimated

MAX_MEGAPIXELS: 20 isn’t a number picked from a spec — this API’s codec decodes at full size before any resize, so a 20 MP source is roughly 80 MB of RGBA sitting in an isolate whose memory ceiling (128 MB) also has to hold the wasm heap, the original source bytes, and the encoder’s own working set. Whether that actually fits was checked directly: a real 5477×3651 (20.00 MP) JPEG at quality 85 (9.03 MB) was posted against a running instance 30 times across three separate runs — 5 requests sequential, 5 concurrent, per run — targeting 30 KB. All 30 requests returned 200, converging on the identical 28,357-byte result at 7 encode attempts every time, deterministic across all three runs. No crash, no OOM, at the full configured 20 MP ceiling.

One honest caveat carries forward from that measurement: it ran against local wrangler dev, and the 5-concurrent phase’s wall times formed a clean staircase (roughly 15s / 30s / 45s / 61s / 79s) — strongly suggesting local workerd serialises CPU-bound synchronous work rather than running it in true parallel across concurrent requests. That’s a property of the local dev runtime, not a claim about Cloudflare’s edge: real concurrent-isolate behaviour under load has not yet been verified against a deployed instance. Treat “20 MP holds” as proven for a single request’s resource footprint, and treat concurrent-load behaviour at the edge as still open.

Why the wall clock isn’t the real bound

WALL_CLOCK_MS: 25000 exists, is threaded into an AbortController, and an abort is mapped to 503 processing-timeout when it fires — but don’t read that as “no request runs longer than 25 seconds.” A wall-clock deadline cannot reliably bound a Worker that performs no I/O:

  • runJob’s decode-and-search chain over an in-memory buffer never awaits anything that yields to a macrotask, so the setTimeout backing the abort may never get a turn to run during an unbroken stretch of synchronous compute.
  • Cloudflare documents that Date.now() inside a Worker returns the time of the last I/O, frozen for the duration of a pure-compute stretch — and this API’s codecs are all statically bundled, so a compress job performs no I/O at all once it starts. A Date.now() >= deadline check may never observe the deadline passing during the compute itself.

This was confirmed empirically, not just reasoned about: in the same load measurement above, the 5th concurrent request took 79 seconds of wall time — more than 3× the configured 25-second budget — and still returned a clean 200, never the 503 the abort machinery is supposed to produce. The abort path is correct on the occasions it does fire (a request whose deadline had already passed before the job even started, for instance), but it is not the mechanism actually protecting this endpoint’s cost.

What actually bounds a running job’s cost is everything else this API already refuses on: MAX_MEGAPIXELS bounds the pixel cost of a single decode/encode, MAX_ENCODES bounds the number of probes a target search can run (what bounds cost accumulating across a slow search), and the platform’s own cpu_ms: 60000 limit is the final backstop — a different mechanism entirely, an isolate-level kill this API’s own code can’t observe or improve on. If you need a client-side timeout, set one on your own request — don’t rely on 503 processing-timeout arriving reliably.

Was this page helpful?