You can render share cards inside a Cloudflare Worker with Satori and resvg-wasm, but the working setup depends on how the WebAssembly is bundled and loaded, not only on which libraries you choose. Satori turns a layout tree into SVG, and resvg converts that SVG into PNG. The hard part is the Worker runtime. Robert Gordon’s write-up of building the card generator for Commit Archive reports four separate failures: a WebAssembly loading error, a fetch receiver bug that appeared only in a queue consumer, an asynchronous GitHub API response, and an account-level request limit that broke several Workers at once.
Everything below comes from that one production account. Commit Archive is a single Worker application that uses Next.js through OpenNext, D1, Queues and R2. Where a number or behavior is specific to that app, this guide says so. Where it reflects documented Cloudflare or Satori behavior, it is labeled that way.
As an Amazon Associate I earn from qualifying purchases.
How the rendering chain works
Gordon’s templates are plain { type, props } object trees rather than React components. That lets the same renderer run from an API route and from a queue consumer without React in the job path. The chain has two stages:
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →- Satori converts the element tree into an SVG string.
- @resvg/resvg-wasm converts that SVG into a PNG.
Satori converts text into SVG path data by default, so glyph outlines are embedded in the SVG itself. Text rendering also needs explicit font data: an ArrayBuffer in web builds, or a Buffer in Node.js. Satori implements a subset of browser layout and CSS rather than a full browser, and it does not guarantee that its output matches browser-rendered HTML. Design cards against what Satori supports, and check them visually.
#1 Best Overall
Card sizes and reported performance
Commit Archive uses two card formats. Neither size is imposed by Cloudflare or Satori; they are the sizes this application chose.
| Card | Dimensions | Reported average render time | Reported PNG size |
|---|---|---|---|
| Project Open Graph card (one per archived project) | 1200×630 px | About 56 ms, warm isolate | About 38 KB |
| Contributor card (portrait) | 1080×1350 px | About 82 ms, warm isolate | About 42 KB |
Gordon also reports about 93 ms of WebAssembly initialization, paid once per isolate. These are measurements from his application, and the write-up does not state the test hardware or sample size. Treat them as an order-of-magnitude reference, not a benchmark. The write-up is dated September 16, but the year does not appear on the page.
Platform facts and one team’s observations
Before setting anything up, separate what the platform and libraries document from what Gordon observed in his own deployment.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →| Point | Source | Scope |
|---|---|---|
Workers can run WebAssembly through WebAssembly.instantiate() with precompiled modules |
Cloudflare Workers WebAssembly documentation, updated April 23, 2026 | Platform |
| Each Worker runs in a single thread; threading and the Web Worker API are not supported | Cloudflare Workers WebAssembly documentation | Platform |
WASM dependencies typically increase Worker size and may increase startup time; wasm-opt is recommended for binary size |
Cloudflare Workers WebAssembly documentation | Platform |
| TTF, OTF and WOFF are supported; WOFF2 is not | Satori README | Library |
Standalone Satori builds leave out Yoga’s WASM binary, which you supply and initialize with init before rendering |
Satori README | Library, general standalone guidance |
Pinning Satori to 0.15.x, using the satori/wasm entry with yoga-wasm-web, and importing WASM through a CompiledWasm rule |
Gordon’s Commit Archive setup | One application, one version |
| Newer Satori versions failing on import in that setup | Gordon’s Commit Archive setup | Version and build specific |
Community packages such as @cf-wasm/og wrapping Satori and resvg for Workers |
Cloudflare WASM Modules repository (community project, not an official Cloudflare endorsement) | Third-party option |
Setup: bundling WASM and loading fonts
The platform allows precompiled WebAssembly modules, but the Satori standalone path assumes you can fetch and initialize Yoga at runtime. Gordon’s arrangement compiles the modules at build time instead. In outline:
- Pin Satori to the 0.15.x line in your project. Gordon’s report ties this pin to the failure described below, so confirm the version you install.
- Import the
satori/wasmentry and provide Yoga throughyoga-wasm-web. - Import the Yoga and resvg WebAssembly modules through a
CompiledWasmrule inwrangler.jsonc, so they are compiled as part of the build. - Vendor the font files you need as TTF, since WOFF2 is not supported. Gordon shares the same font files with his website, so a single source of truth covers both.
- Run the Worker with
wrangler devand render a card before deploying anything.
Keep the font step deliberate. A font that works in a browser may be WOFF2 by default, and Satori will not read it. Converting or sourcing TTF files early avoids a late surprise.
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.
Rank #2
Cause, as Gordon diagnosed it. The newer Satori release he tried used harfbuzzjs. That library tried to locate its WebAssembly through location.href at import time, which failed in his Worker setup. The fix he describes was the pinned 0.15.x line, the satori/wasm entry, and compiled imports for Yoga and resvg.
Recommended Free Tools
What to verify. Check which Satori version resolves in your lockfile, and do not assume a newer release behaves the same. Run the card route under wrangler dev. A successful run in Node proves only that the code is correct in Node.
2. TypeError: Illegal invocation, only in the queue consumer
Symptom. Card generation succeeded through the Next.js request path but threw Illegal invocation inside the queue consumer.
Cause, as Gordon diagnosed it. His GitHub client stored fetch as a method and later called it as this.fetchImpl(url). The Next.js request path patched globalThis.fetch, which masked the receiver problem. The raw Worker queue entry did not have that patch, so the bug surfaced there.
Fix. Wrap the call in a free function so the native function is not invoked with the wrong receiver: ((input, init) => fetch(input, init)).
What to verify. Exercise every entry point that calls outbound fetch, not only the request handler. Gordon’s report is a single diagnosis, so confirm the call path in your own code.
Rank #3
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 timing. For one test repository, the statistics took about 15 minutes to appear, and Gordon’s initial job exhausted its five retries. This was one observed case, not a guaranteed processing time, and other repositories may resolve faster or slower.
Change in product behavior. From the first retry onward, the job published the project without line counts. A later scheduled refresh filled in the counts once GitHub had computed them. Gordon treats the 202 as a pending state to handle, not as a failure that stops the job.
What to verify. Design the publish step so it can complete without this data, and make sure the refresh path can update it later. Test with a repository whose statistics have not been requested recently.
4. Cloudflare error 1027, “temporarily rate limited”
Symptom. Requests failed with Cloudflare error 1027 across environments at about the same time, which made the problem look like a bug in Commit Archive.
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 whole account at that time.
Rank #4
Mitigation. He moved the other Worker off its public route, then moved the account to Workers Paid. The upgrade also raised the CPU limit, which matters for large ingestion jobs.
What to verify. Check the account’s Workers analytics for a neighboring Worker before debugging your own code when several Workers fail together. Plan quotas and prices change, and the Cloudflare WebAssembly documentation does not confirm the 100,000-request figure. Read the current plan documentation before relying on any quota.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Caching and edition state
Gordon’s app stores rendered cards in R2. Live cards use short cache headers because their content can still change. Once an edition is sealed, its cards become immutable and can be cached for longer. He does not benchmark this against other strategies, so treat it as a pattern to adapt, not a tested optimum.
Verification checklist
Gordon’s practical lesson is worth quoting in full: “Lesson: test the renderer under wrangler dev, not only in Node.” Before you ship a share-card renderer on Workers, check each item:
- Render at least one card of each size under
wrangler devand compare the PNG output with what you expect. - Confirm the resolved Satori version and whether it matches the bundling setup you tested.
- Confirm that Yoga and resvg are imported as compiled modules, and that the build produces them without runtime fetches.
- Confirm every font file is TTF or OTF (or WOFF, if you use that format), not WOFF2.
- Exercise the request route, the queue consumer and any scheduled job, since each can expose a different
fetchor runtime behavior. - Handle a GitHub 202 response as pending. Make sure publish can complete without the data and that a later job fills it in.
- Record cold and warm render times, PNG sizes and WASM initialization time for your own cards, not the figures quoted here.
- Check account-level request and CPU limits against the current plan documentation, and look for other Workers on the same account.
The sequence matters. Gordon’s failures came from runtime differences that a Node test would never reveal, so the Worker runtime should be the environment that signs off on the renderer.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsQuick Recap
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.




