Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

You can render share-card PNGs inside a Cloudflare Worker with Satori and resvg-wasm, but the working route is narrower than the libraries’ general documentation suggests. Satori turns a layout tree into SVG, @resvg/resvg-wasm turns that SVG into PNG, and the chain only runs reliably in the Workers runtime once the WebAssembly is bundled and loaded the way workerd expects. The most detailed production account available is Robert Gordon’s write-up of Commit Archive, a single Cloudflare Worker that generates Open Graph images on demand. This guide sets out his implementation path, the four failures he reported, what is general platform behaviour and what is specific to his application, and a checklist for testing the same setup in your own deployment.

What Gordon built and what it proves

Commit Archive generates a 1200×630 Open Graph image for each archived project and a 1080×1350 contributor card, both on demand. Gordon’s application runs as one Cloudflare Worker using Next.js through OpenNext, with D1, Queues and R2. Rendered cards are cached in R2. Live cards are served with short cache headers, and cards become immutable once their edition is sealed.

Those dimensions and cache rules are his application’s choices, not requirements from Cloudflare or Satori. The write-up is dated 16 September, and the page does not show a year, so treat the details as a snapshot of that codebase rather than a dated platform specification.

The rendering pipeline

  1. Build the card as a plain { type, props } object tree. Gordon does this so the same renderer can be called from an API route and from a queue consumer without React in the job path.
  2. Pass the tree to Satori, which converts it to an SVG string. Text is embedded as glyph outlines (SVG path data) by default, so the output does not depend on fonts being installed on the client.
  3. Pass the SVG to @resvg/resvg-wasm, which renders it to PNG at the card’s pixel size.
  4. Return the PNG directly for live requests, or write it to R2 for cached and sealed cards.

What Satori does and does not do

Satori accepts JSX or React-element-like objects, but it implements a subset of HTML and CSS rather than a full browser. Its README says it cannot guarantee that output matches browser-rendered HTML exactly, so design cards against the subset it documents and check the result visually.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Satori accepts TTF, OTF and WOFF font files. It does not support WOFF2, so a font that works in a website’s CSS may still fail in the renderer. Text rendering needs explicit font data: an ArrayBuffer in the web build or a Buffer in Node.js.

Font delivery in Gordon’s setup

Gordon vendors TTF files into the repository and shares the same font files with the website. This keeps the card typography reproducible across builds, and it avoids a runtime font request inside the Worker. The trade-off is that font files become part of the bundle, so their size counts toward the WebAssembly and deployment footprint you are already managing.

Build and runtime setup that runs under workerd

Gordon’s working configuration differs from the simplest Satori example in three ways:

  • Satori is pinned to the 0.15.x line. He imports the satori/wasm entry and pairs it with yoga-wasm-web.
  • Yoga and resvg are imported as compiled modules. A CompiledWasm rule in wrangler.jsonc makes the .wasm files importable as precompiled modules:
{
  "rules": [
    { "type": "CompiledWasm", "globs": ["**/*.wasm"] }
  ]
}
  • resvg is initialised once per isolate with the compiled module before the first render. Gordon measured this initialisation at about 93 ms per isolate.

Cloudflare’s WebAssembly documentation, last updated 23 April 2026, confirms that Workers can instantiate precompiled modules through WebAssembly.instantiate(). It also notes that each Worker runs in a single thread, that threads and the Web Worker API are not supported, and that WASM dependencies usually increase Worker size and can increase startup time. It recommends wasm-opt for reducing binary size.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A community project, the Cloudflare WASM Modules repository, lists @cf-wasm/og as a dynamic Open Graph renderer built on Satori and resvg, with separate Satori and resvg packages listed as compatible with Workers. It is a third-party option, not an official Cloudflare endorsement, and Gordon’s write-up does not use it.

The four failures

1. “Wasm code generation disallowed by embedder”

Symptom. The renderer worked under Node but failed on workerd with the error Wasm code generation disallowed by embedder.

Cause, as Gordon diagnosed it. Newer Satori versions, in his testing, used harfbuzzjs, which tried to locate its WASM file through location.href at import time. That lookup failed inside this Worker setup.

Mitigation he used. Pin Satori to 0.15.x, import through satori/wasm with yoga-wasm-web, and import the Yoga and resvg modules through the CompiledWasm rule shown above.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What to verify. This is version- and build-specific. Check the Satori package documentation for the version you install, and do not assume a newer release behaves the same way. The general lesson from his report is that a Node success does not prove workerd compatibility. Gordon’s own conclusion was: “Lesson: test the renderer under wrangler dev, not only in Node.”

2. “TypeError: Illegal invocation”, only in the queue consumer

Symptom. Card generation succeeded through the Next request handler but threw TypeError: Illegal invocation when run from the queue consumer.

Cause, as Gordon diagnosed it. A GitHub client stored fetch as a method and later called it as this.fetchImpl(url). Gordon reports that Next’s request path patches globalThis.fetch, which masked the receiver problem. The raw Worker queue entry exposed it.

Mitigation. Wrap the call as a free function so the receiver is not bound to the client object:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
((input, init) => fetch(input, init))

What to verify. This is an author-reported diagnosis. Exercise each entry path separately, including the queue consumer, and confirm the failing call in your own runtime before applying the wrapper.

3. GitHub contributor statistics return HTTP 202

Symptom. GET /repos/{owner}/{repo}/stats/contributors returned 202 Accepted with no body while GitHub computed the statistics.

Observed behaviour. For one test repository, the statistics took about 15 minutes to appear, and the initial job exhausted five retries. This is Gordon’s single reported example, not a guaranteed processing time.

Mitigation. From the first retry onward, the application published the contributor card without line counts. A later scheduled refresh filled in the counts once GitHub had them. Gordon changed the product flow rather than treating the pending response as a fatal job error.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What to verify. Decide what the card shows while statistics are pending, and make the refresh path idempotent so a later run can complete the card.

4. Error 1027 from a different Worker

Symptom. Requests failed with Cloudflare error 1027, which Gordon quotes as “temporarily rate limited.” The failures appeared across environments at about the same time, which made the rendering code look responsible.

Cause, as Gordon diagnosed it. Another Worker on the same account was generating a few hundred thousand requests per day. Gordon reports that the Free plan’s 100,000 daily requests were shared across the account at the time.

Mitigation. He moved the other Worker off its public route. He later moved the account to Workers Paid, which also raised the CPU limit that matters for large ingestion jobs.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What to verify. The Cloudflare WebAssembly page does not confirm these quota figures, and plan limits change. Check the current plan documentation in your dashboard before treating either number as today’s limit.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance figures from one workload

Gordon measured the following on warm isolates in his own application. These are not comparative benchmarks and do not describe cold starts or other workloads.

Card Pixel size Average render time Average PNG size
Project Open Graph card 1200×630 About 56 ms About 38 KB
Contributor portrait card 1080×1350 About 82 ms About 42 KB

Caching pattern

Gordon’s application caches rendered PNGs in R2. Live cards carry short cache headers because their data can change, and cards are marked immutable after the edition is sealed. He does not compare this with other caching strategies, so the pattern is a reasonable starting point rather than a tested optimum.

Verification checklist for your own deployment

  • Run every card type under wrangler dev and compare the output against a visual reference. Use Node only as a secondary check.
  • Exercise the API route and the queue consumer as separate entry points, and confirm each makes its outbound fetch calls without errors.
  • Inspect the built bundle to confirm the .wasm files are imported as compiled modules, and confirm the Satori and Yoga versions match the ones you tested.
  • Confirm every font file is TTF, OTF or WOFF, not WOFF2.
  • Handle GitHub’s 202 response as a pending state, with a scheduled retry that completes the data later.
  • Check the account for other Workers that might be consuming shared daily requests, and confirm current plan limits in the Cloudflare dashboard.
  • Record cold-start and warm render timings for your own card sizes and data volume. Run wasm-opt on the WebAssembly to reduce size.

Before copying the setup, decide the choices that matter for your project: whether to write templates as JSX or plain object trees, whether you need SVG or PNG output for the platforms you target, how long cards should stay cached, and whether to vendor fonts or load them another way.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.