The JavaScript ecosystem has evolved to offer alternative runtime environments to Node.js that promise greater efficiency, instant cold starts, and native integration with the web platform. But when a development team decides to leave Node behind and adopt workerd —the open-source runtime behind Cloudflare Workers— they face an unforgiving reality: Node SDKs, designed for a world with a filesystem, native modules, and persistent in-memory state, simply break. In this article we share our real experience migrating a SaaS backend to workerd, the failures we encountered, the solutions we implemented, and the lessons every technical team should consider before embarking on a similar path.
At Q2BSTUDIO, a software and technology development company specialized in custom software, we are used to making architectural decisions that maximize flexibility and minimize operational costs. Our main product is an AI-powered support assistant deployed as a serverless backend. After months running on Node.js, we noticed that cold start times, container management, and dependency on Docker images were hindering both development and user experience. We decided to migrate to workerd, lured by the promise of a universal runtime with near-instant cold starts and atomic deployments. What we did not anticipate is that most of the third-party SDKs we used —Stripe, Resend, Better Auth— would break categorically in the new environment.
The fundamental problem is that workerd implements the Web Platform standard: fetch, Request/Response, crypto.subtle, TextEncoder, streams. It does not offer Node's standard library, a filesystem, or persistent module-scoped state between requests. Node SDKs, even those with a clean public API, drag in deep dependencies that invoke native modules like crypto, fs, or http. When workerd's bundler encounters those dependencies, compilation fails. There is no room for 'works on my machine': the error occurs at build time, not in production. It is brutal, but brutal in CI, not at 3 AM.
The first SDK to fall was Resend's, our transactional email provider. The official npm package looked harmless, but it pulled in svix, a webhook verification library that in turn required Node modules. Since sending an email is a single HTTP call, we replaced it with a direct fetch to Resend's API. The resulting code is minimal: a POST request with a Bearer header and JSON body, no client, no retry machinery, no dependency tree. We also implemented a small address validator that rejects the $ symbol —not for RFC correctness, but to catch unexpanded environment variables that would cause 422 errors. The lesson: sometimes simpler works better, and removing a third-party SDK can reduce the attack surface and improve maintainability.
The second blow came from Stripe's Node SDK, the core of our usage-based billing. Stripe expects form-encoded bodies with a bracket convention for nesting —line_items[0][price]=price_123— that the SDK abstracts away. Without that layer, we had to implement a manual recursive encoder. More critical: Stripe's webhook verification uses HMAC-SHA256 and requires constant-time comparison to avoid information leakage. We wrote our own verifyStripeSignature function, which extracts the timestamp and signature from the stripe-signature header, computes the HMAC with crypto.subtle, and compares in constant time, rejecting signatures older than 300 seconds. We replicated this pattern for Resend webhooks, but with subtle differences: Svix signs ${id}.${timestamp}.${body}, uses a base64-encoded key, and produces space-separated signatures. Two schemes, two hand-rolled verifiers that we now maintain. The real cost of leaving the SDK behind is that the vendor's docs, not their code, become your specification.
The third SDK that did not survive was Better Auth's passkey plugin. We never even called the offending code: the plugin depended on @simplewebauthn/server, which depended on @peculiar/x509 and asn1js —an X.509 certificate parser needed for WebAuthn attestation verification. On workerd, that dependency chain broke the bundle. The rest of Better Auth runs fine; only the passkey plugin was removed before launch. The moral: the dependency that kills your deployment is rarely in your package.json; it is often the certificate parser your auth plugin needs for a flow you may never exercise.
Beyond broken SDKs, workerd imposes subtle changes to the execution model. Environment variables arrive as bindings per request, not as process.env at module scope. So we construct the auth client on each request: createAuth(env). In-memory state is a mirage because workerd distributes traffic across short-lived isolates; Better Auth's default in-memory rate limiter becomes decorative. We replaced it with a counter backed by the KV state binding, which survives across isolates. We also discovered there is no filesystem, so our embeddable widget is served as a generated string constant at build time, versioned atomically with the serving API.
The lack of atomicity in KV forced us to explicitly decide how to fail at each point. The rate limiter on public endpoints fails open —if the store is unavailable, we allow the request— because we do not want infrastructure issues to block all customers. The idempotency reservation on the billing path fails closed —if we cannot verify state, we block the operation— to avoid duplicates involving money. Every failure decision is documented in the code, and the team knows exactly what to expect during a partial outage.
Not everything broke. Hono, the HTTP framework we use, runs natively on workerd. Drizzle connects smoothly to the D1-compatible SQLite database. Zod, the schema validator, is pure JavaScript. Better Auth's core works. The Vercel AI SDK and our AI gateway provider bundle without errors. The web-standard ecosystem is solid; the problem concentrates in vendor SDKs with deep dependency trees, and you cannot predict them by reading your own import statements.
From a business perspective, migrating to workerd gave us deployments that cannot half-work: if it bundles, it runs. The dependency surface shrank dramatically; the Stripe integration went from an entire SDK to 289 lines that any developer can read in one sitting. Every HTTP call of our billing system is visible in one file. Web-standard portability allows the same code to run on Cloudflare Workers, Deno, or any runtime that implements the web platform —which fits our self-hosting model. However, we paid a price: we are now the maintainers of clients that vendors used to maintain. When Stripe ships a new API version, nobody bumps a package for us; we read the changelog and edit stripe.ts. We own security-sensitive code —two webhook verifiers with constant-time comparison— that we must get right, test, and keep right.
The trade-off, for a small team with a simple API surface and strong tests, is positive. But if your backend touches twenty vendor APIs with fast-moving surfaces, hand-rolling twenty clients is a much worse deal, and a Node runtime that runs the official SDKs is defensible. If you do take the leap, start where we should have: not with your package.json, but with npm ls --all and a hard look at what your dependencies' dependencies drag in. The SDK that breaks your deployment is never the one you imported.
At Q2BSTUDIO, we apply the same philosophy to all our cloud AWS/Azure projects, where runtime choice and dependency management are critical for scalable and secure solutions. We also integrate cybersecurity services to protect webhooks and API keys, and use Business Intelligence with Power BI to monitor system behavior in real time, detecting usage patterns and potential anomalies. Our AI agents benefit from a lightweight runtime that enables fast responses without the overhead of heavy containers. The final lesson: migrating to workerd is not a technical whim, but a strategic decision that requires auditing not just your imports, but your entire transitive dependency tree. When you do it right, you gain speed, reliability, and control. When you don't, you learn —as we did— that what breaks is never what you expect.




