<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sandeep Singh</title>
    <description>The latest articles on DEV Community by Sandeep Singh (@gurusandeep).</description>
    <link>https://dev.to/gurusandeep</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4068587%2F4afdfce8-ccd8-4f0a-bbd3-fc067abc3c28.jpg</url>
      <title>DEV Community: Sandeep Singh</title>
      <link>https://dev.to/gurusandeep</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gurusandeep"/>
    <language>en</language>
    <item>
      <title>Why Nodemailer Doesn't Work on Cloudflare Workers (And What To Do Instead)</title>
      <dc:creator>Sandeep Singh</dc:creator>
      <pubDate>Sat, 08 Aug 2026 09:21:49 +0000</pubDate>
      <link>https://dev.to/gurusandeep/why-nodemailer-doesnt-work-on-cloudflare-workers-and-what-to-do-instead-358h</link>
      <guid>https://dev.to/gurusandeep/why-nodemailer-doesnt-work-on-cloudflare-workers-and-what-to-do-instead-358h</guid>
      <description>&lt;p&gt;&lt;em&gt;A short explanation of a wall a lot of developers hit, why it isn't going away, and the five lines that replace it.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;You wrote a contact form. It worked locally. You deployed it to a Cloudflare Worker, or a Vercel Edge Function, or Deno Deploy, and got something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeError: Class extends value #&amp;lt;Object&amp;gt; is not a constructor or null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you were luckier and got a useful error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Module not found: Can't resolve 'net'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you spent an hour trying compatibility flags, polyfills, and bundler aliases. I want to save you the rest of that hour.&lt;/p&gt;

&lt;p&gt;This isn't a bug, and no amount of configuration will fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual reason
&lt;/h2&gt;

&lt;p&gt;Nodemailer's default transport is SMTP. SMTP is a protocol that runs over a raw TCP connection. To open one in Node.js, you call &lt;code&gt;net.createConnection()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Cloudflare Workers don't run on Node.js. They run on V8 isolates — the same engine as Chrome, without the Node runtime around it. Vercel's Edge Runtime and Deno Deploy are built on similar principles.&lt;/p&gt;

&lt;p&gt;In that environment, there is no &lt;code&gt;net&lt;/code&gt; module, because there are no raw TCP sockets. All networking is handled by managed infrastructure &lt;em&gt;outside&lt;/em&gt; the runtime — Cloudflare's own writeup on bringing &lt;code&gt;node:http&lt;/code&gt; to Workers is explicit about this: connection pooling, TLS negotiation, and egress IP management are handled at the system level, which is precisely why a subset of Node APIs can never be supported.&lt;/p&gt;

&lt;p&gt;So the chain is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No raw TCP → no &lt;code&gt;net.createConnection()&lt;/code&gt; → no SMTP client → no Nodemailer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's a second, smaller issue that often gets conflated with this one. &lt;a href="https://github.com/nodemailer/nodemailer/issues/1621" rel="noopener noreferrer"&gt;Nodemailer issue #1621&lt;/a&gt; points out that Nodemailer imports built-in modules without the &lt;code&gt;node:&lt;/code&gt; prefix, which breaks the Workers build step. That one is fixable. But fixing it wouldn't help — you'd just move the failure from build time to runtime, where &lt;code&gt;net&lt;/code&gt; still doesn't exist. &lt;a href="https://github.com/nodemailer/nodemailer/issues/1623" rel="noopener noreferrer"&gt;Issue #1623&lt;/a&gt; covers the broader edge-function problem.&lt;/p&gt;

&lt;p&gt;It's worth being clear that none of this is a knock on Nodemailer. It's an excellent library, actively maintained, MIT-0 licensed, with zero runtime dependencies and millions of weekly downloads. It does one job extremely well: constructing a correct MIME message and getting it onto a wire. The edge simply doesn't have the wire.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do instead
&lt;/h2&gt;

&lt;p&gt;Use an HTTP API. Every transactional email provider has one, and HTTP is the one thing edge runtimes are exceptionally good at.&lt;/p&gt;

&lt;p&gt;Here's a complete Cloudflare Worker that sends email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.yourprovider.com/v1/send&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EMAIL_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello@yourdomain.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello from the edge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;p&amp;gt;It works.&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Send failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No dependency. No polyfill. No bundler config.&lt;/p&gt;

&lt;p&gt;Store the key with &lt;code&gt;wrangler secret put EMAIL_API_KEY&lt;/code&gt; so it never lands in your repo or your deploy logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody says out loud
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;On the edge, you don't need an email library at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's a growing set of runtime-agnostic email libraries built specifically to fill this gap, and they're well made. But be honest with yourself about what you're buying. If you're sending a handful of transactional message types from a Worker, &lt;code&gt;fetch&lt;/code&gt; is the whole solution. A wrapper around &lt;code&gt;fetch&lt;/code&gt; earns its place when you need provider-swapping, a unified message type across a large codebase, or heavy MIME construction with attachments and inline images.&lt;/p&gt;

&lt;p&gt;For a password reset and a welcome email? Ship the five lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Nodemailer / SMTP&lt;/th&gt;
&lt;th&gt;What to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node.js (Express, Fastify, NestJS)&lt;/td&gt;
&lt;td&gt;✅ Works&lt;/td&gt;
&lt;td&gt;Nodemailer, or an HTTP SDK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js — route handlers, server actions&lt;/td&gt;
&lt;td&gt;✅ Works (Node runtime)&lt;/td&gt;
&lt;td&gt;Nodemailer, or an HTTP SDK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js — Edge Runtime&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fetch&lt;/code&gt; to an HTTP API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js — middleware&lt;/td&gt;
&lt;td&gt;❌ (always edge)&lt;/td&gt;
&lt;td&gt;Don't send email here at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Lambda, containers&lt;/td&gt;
&lt;td&gt;✅ Works&lt;/td&gt;
&lt;td&gt;Nodemailer, or an HTTP SDK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Workers&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fetch&lt;/code&gt; to an HTTP API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deno Deploy&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fetch&lt;/code&gt; to an HTTP API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bun&lt;/td&gt;
&lt;td&gt;⚠️ Mostly works&lt;/td&gt;
&lt;td&gt;Test it; &lt;code&gt;fetch&lt;/code&gt; is safer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two notes on the Next.js rows, because this trips people up constantly.&lt;/p&gt;

&lt;p&gt;Route handlers and server actions run on the &lt;strong&gt;Node.js runtime by default&lt;/strong&gt; — Nodemailer works fine there. You only lose it if you've explicitly opted into &lt;code&gt;export const runtime = 'edge'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Middleware is &lt;em&gt;always&lt;/em&gt; edge. Even setting aside the runtime, middleware runs on every matched request with tight execution limits. Sending email from it is a bad idea for reasons that have nothing to do with SMTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  While you're in here: the duplicate email bug
&lt;/h2&gt;

&lt;p&gt;This one bites people right after they solve the edge problem, and it's worth fixing in the same sitting.&lt;/p&gt;

&lt;p&gt;Serverless platforms retry on timeout. If your function calls the email API, the send succeeds, and then the function times out on something downstream, the platform re-invokes it. The email goes out twice.&lt;/p&gt;

&lt;p&gt;Your user gets two password reset emails with two different tokens. Now they're confused, and one of those tokens is a live credential floating in an inbox.&lt;/p&gt;

&lt;p&gt;The fix is an idempotency key — a stable identifier sent with the request, which the provider uses to deduplicate for some window. Retry with the same key, get the original result back instead of a second send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`pwreset-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Derive it from something stable about the &lt;em&gt;request&lt;/em&gt;, not from &lt;code&gt;Date.now()&lt;/code&gt; or a fresh UUID — those change on retry, which defeats the entire mechanism.&lt;/p&gt;

&lt;p&gt;Not every provider supports this. It's worth checking before you pick one, because you cannot build it yourself in a stateless function. Deduplication requires state, and your function doesn't have any.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Nodemailer can't run on edge runtimes because those runtimes have no TCP sockets. This is architectural and permanent, not a bug awaiting a fix.&lt;/li&gt;
&lt;li&gt;On the edge, call an HTTP API with &lt;code&gt;fetch&lt;/code&gt;. It's five lines.&lt;/li&gt;
&lt;li&gt;In Node — including most Next.js code — Nodemailer is still great.&lt;/li&gt;
&lt;li&gt;Check your provider supports idempotency keys before serverless retries send your users duplicate emails.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>nextjs</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
