A leading product engineering company, creating adaptive software solutions to improve operations, providing businesses with expert development services from across domain.

A leading product engineering company, creating adaptive software solutions to improve operations, providing businesses with expert development services from across domain.

Mobile App Development

Cloudflare Workers production issues worth learning early

Nine Cloudflare Workers production issues from shipping a paid product: the idempotency key that double-charged, silent cron failures, and node compat gaps.

Cloudflare Workers production issues worth learning early

Jack Cooper shipped a paid image product on Cloudflare Workers and wrote down every failure along the way. It is the most useful post about Cloudflare Workers production issues published this year, because he charged money, hit real consequences, and named them rather than writing a tutorial where everything works.

Not all nine are equally instructive. Four are worth your time whatever you build on, and one of them cost him a duplicate charge.

The idempotency mistake worth learning second-hand

Cooper used the event ID as the idempotency key rather than the order ID, and a single purchase was processed twice — one top-up credited twice. His conclusion is the line to keep: pick the identifier of the thing that happened, not the identifier of the message.

The same reasoning is why an Idempotency-Key header is now standard in payment and commerce APIs — the agentic commerce specifications require merchants to honour and echo one on every checkout endpoint, precisely so a retried delivery cannot become a second order.

This is the most common webhook bug in commerce integrations and it survives review because the code looks correct. An event ID is unique per message, so deduplicating on it feels right. But providers legitimately emit multiple events about one business fact — a retry after your endpoint times out, a duplicate delivery, a related event type describing the same payment. Each carries a distinct event ID. Deduplicating on it means you deduplicate nothing that matters.

The key has to identify the business fact: this order, this payment, this subscription period. Then a second delivery about the same fact is recognisably the same fact, whatever envelope it arrived in. We made the same argument working through webhook retry patterns and what Stripe actually guarantees, and it remains the single highest-value thing to get right in a payments integration.

One detail makes it worse in practice than it reads. Most payment providers retry aggressively and will deliver the same event several times if your endpoint is slow, so the volume of duplicate deliveries is highest exactly when your system is already struggling. A deduplication scheme that works under normal load and fails under pressure is the definition of a bad one, and keying on the message is precisely that.

Node compatibility is a spectrum, not a switch

A payment SDK failed with node:crypto not implemented despite nodejs_compat being enabled. He replaced the signing with about thirty lines of WebCrypto.

Worth internalising before committing to an edge runtime. Compatibility flags cover a growing subset of Node's surface, not all of it, and the gaps are concentrated exactly where vendor SDKs live — crypto, streams, some filesystem assumptions. The failure mode is bad because it appears at runtime in a specific code path rather than at build time, so a signing function used only on webhook verification can pass every test and fail in production.

The practical check before choosing the runtime: list your non-negotiable third-party SDKs — payments, auth, storage, email — and verify each actually runs, rather than trusting a compatibility matrix. Thirty lines of WebCrypto is a fine outcome when you know about it in week one and an unpleasant surprise in week six.

There is a testing implication too. A compatibility gap in a rarely exercised path will not surface in local development if your local runtime is Node rather than the edge runtime itself. Running the real runtime locally, and exercising webhook verification and signing paths in CI rather than only the request paths, is what turns this from a production discovery into a first-week one.

Configuration that silently wins

Two failures share a shape, and it is the shape that makes them dangerous.

His cron handler silently failed to execute because wrangler.jsonc pointed at the wrong entry file. No error, no invocation — a scheduled job that simply was not running. The fix was to own the entry point explicitly in src/server.ts rather than inherit the framework package's.

Separately, vars in wrangler.jsonc overwrote values edited in the dashboard on each deploy. A webhook key edited in the dashboard reverted, and transactions failed. The mitigation is to keep genuinely sensitive values in secrets and leave vars as the repository's business, never edited in two places.

Both are the same category: configuration with two sources of truth where one silently wins. It is worth auditing your own deployment for it, because the answer is rarely documented. Anything editable in a dashboard and also declarable in a repository will be overwritten by whichever runs last, and nobody finds out until a value that mattered reverts.

The cron case has a wider lesson about scheduled work generally: a job that does not run produces no signal at all. Errors alert. Absence does not. Every scheduled task worth having is worth a heartbeat that alerts on silence.

There is a broader habit here worth adopting regardless of platform: treat anything that can be set in two places as a defect waiting to happen, and pick one owner deliberately. Environment values, feature flags, scheduled job definitions and DNS records are the usual suspects. It costs an hour to write down which system is authoritative for each, and it is the kind of documentation that pays for itself the first time someone edits the wrong one at midnight.

The browser-side failures still cost money

Two of the nine were front-end and one of them charged a customer twice.

Double-clicking before a render completed caused a double charge, fixed by using a useRef as a synchronous lock rather than relying on React state. This is a genuine React trap rather than a mistake — state updates are asynchronous and batched, so a guard based on state is not yet true when the second click arrives. A ref updates synchronously. Any button that spends money needs the synchronous version, and ideally an idempotency key on the request behind it as well.

The other was subtler: a stylesheet inside route-managed head markup was rebuilt on every navigation, causing a full font re-download visible as a flash of text. Moving it to the document shell, outside route management, fixed it. Framework-managed head elements are re-evaluated per route by design, and anything that should persist for the session belongs in the shell.

He also hit a 20 MB input limit on the Images binding against upscaled PNGs reaching 40 MB, solved by requesting JPEG from providers and resizing at the edge. A reminder that platform limits are architecture constraints, not runtime errors to handle — the fix was upstream of the failure.

What Cloudflare Workers production issues say about edge runtimes

None of this is an argument against Workers. Read the list again and the pattern is that the platform behaved as documented in every case — the limits, the compatibility gaps and the configuration precedence are all written down somewhere. What caught him was that they are not the things you check when the framework is new and the happy path works.

He was also running a release-candidate framework, with the honest note that RC status means no answers on Stack Overflow when something breaks, and that plugin order matters. That compounds everything else: on a mature stack most of these would have been a search away.

The reasonable read for a team choosing a runtime is to separate two questions. Does the platform do what we need — usually yes, and the constraints are published. And can we absorb being early, which is a question about how much of your team's time can go to problems with no existing answer. For a solo product that is a fair trade. For a client deadline it often is not, which is the same judgement we apply to any web application build: the technology risk is rarely the technology, it is the maturity of the ecosystem around it.

It is also worth saying what an edge runtime buys, since the list above reads as a warning. Requests served close to the user, no cold-start penalty of the kind serverless functions are known for, and a pricing model that suits spiky consumer traffic are all real advantages, and none of the nine failures undermine them. The question is never whether the platform is good. It is whether the specific things your product must do are on the supported path.

The four failures worth carrying away transfer everywhere. Key idempotency on the business fact. Verify SDK compatibility before committing to a runtime. Find the configuration that has two owners. Alert on scheduled jobs that go quiet. None are specific to Cloudflare, and all four are cheaper to fix before launch than after a customer is charged twice.

Frequently asked questions

Using the event or message identifier instead of the business fact. Providers emit several events about one payment, each with a distinct event ID, so deduplicating on it prevents nothing. Key on the order, payment or subscription period so repeated deliveries resolve to the same fact.

No. Compatibility flags cover a large subset of Node's surface but not all of it, and gaps cluster in crypto, streams and filesystem assumptions where vendor SDKs live. A payment SDK failing with node:crypto not implemented was resolved with about thirty lines of WebCrypto.

Because wrangler.jsonc pointed at the wrong entry file, so the scheduled handler was never invoked and no error was produced. Owning the entry point explicitly in your own server file rather than inheriting a framework package's avoids it.

Values declared as vars in wrangler.jsonc overwrite dashboard edits each time you deploy. Keep sensitive values in secrets and treat vars as owned by the repository, so configuration never has two sources of truth where one silently wins.

Use a ref as a synchronous lock rather than React state, because state updates are asynchronous and batched so a state-based guard is not yet true when the second click arrives. Pair it with an idempotency key on the request itself.

20 MB. Upscaled PNGs reaching 40 MB exceeded it, and the fix was upstream rather than in error handling: request JPEG from providers and resize at the edge using the Images binding rather than attempting to process oversized input.

Written by

Akash Mohapatra

Akash Mohapatra

Co Founder & Director

12 Sep 2026

·

8 min read

Share

LET'S CONNECT

Connect with Creuto!

Ready to take the first step towards unlocking opportunities, realizing goals, and embracing innovation? We're here and eager to connect.

Contact Us

We don't just aim to fit in – we strive to stand out. Experience the perfect blend of innovation, excellence, and trust that makes us truly unforgettable. Discover the difference with Creuto.

© 2026 Creuto All Rights Reserved