{"id":359,"date":"2026-09-06T14:17:02","date_gmt":"2026-09-06T19:47:02","guid":{"rendered":"https:\/\/photonconsole.com\/blog\/?p=359"},"modified":"2026-09-06T14:17:03","modified_gmt":"2026-09-06T19:47:03","slug":"how-to-send-emails-in-node-js-with-nodemailer-a-production-setup-guide","status":"publish","type":"post","link":"https:\/\/photonconsole.com\/blog\/how-to-send-emails-in-node-js-with-nodemailer-a-production-setup-guide\/","title":{"rendered":"How to Send Emails in Node.js with Nodemailer: A Production Setup Guide"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Your Node.js app sends email perfectly on localhost. You deploy it, and the emails stop arriving. No crash, no error in the logs, no bounce message. Just silence.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the most common email failure in Node.js applications, and it is almost never a bug in your code. It is a configuration gap between your development machine and your production environment. Most SMTP errors occur due to misconfiguration or authentication issues rather than faults in the application itself.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide walks through a Nodemailer setup that survives production: correct ports, safe credentials, connection pooling, retry handling, and the point at which you need to stop using a personal mailbox and move to a dedicated relay.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Quick Answer: How Do You Send Email in Node.js?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Install Nodemailer, create a single reusable transporter with your SMTP host, port and credentials, then call <code>sendMail()<\/code>.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Run <code>npm install nodemailer<\/code><\/li>\n\n\n\n<li>Create one transporter using <code>nodemailer.createTransport()<\/code> with host, port 587, and auth credentials<\/li>\n\n\n\n<li>Store credentials in environment variables, never in code<\/li>\n\n\n\n<li>Call <code>transporter.verify()<\/code> at startup to confirm the connection<\/li>\n\n\n\n<li>Send with <code>await transporter.sendMail({ from, to, subject, html })<\/code><\/li>\n\n\n\n<li>Enable <code>pool: true<\/code> so connections are reused instead of reopened per message<\/li>\n\n\n\n<li>Use an authenticated SMTP relay rather than port 25 or a personal Gmail account<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The setup takes about ten minutes. The reason it fails in production is usually port 25 being blocked, a missing app password, or a transporter created inside a request handler.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For production traffic, point that transporter at a dedicated relay rather than a personal mailbox. Using an SMTP service like PhotonConsole means the same six lines of Nodemailer code work at 10 emails a day or 10,000.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What Nodemailer Actually Does<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nodemailer is a Node.js module that speaks SMTP on your behalf. It does not deliver email itself. It opens an authenticated connection to a mail server you specify, hands over your message, and that server handles the actual delivery to the recipient.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This distinction matters. Nodemailer is a client, not a mail server. Its job is to format your message correctly and transmit it. Whether that message reaches the inbox depends entirely on the server you connect it to and on your domain&#8217;s authentication records.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you are new to the underlying protocol, our explanation of <a href=\"https:\/\/photonconsole.com\/blog\/what-is-smtp-direct-answer\/\">what SMTP is and how it works<\/a> covers the fundamentals this guide builds on.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why Node.js Email Fails in Production but Works Locally<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Six causes account for the overwhelming majority of these failures.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Port 25 is blocked by your host<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Almost every cloud provider blocks outbound port 25 by default to prevent spam. Code that works on your laptop silently fails on a deployed server. Use port 587 instead.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. Gmail rejects your password<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Google removed support for basic &#8220;less secure app&#8221; sign-in. A regular account password will not authenticate. You need either an App Password generated on an account with two-factor authentication enabled, or OAuth 2.0.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. You create a new transporter on every request<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Calling <code>createTransport()<\/code> inside a route handler opens a fresh TLS handshake for every single email. Under load this exhausts connections and triggers rate limiting from your provider.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. There is no retry logic<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">SMTP failures are frequently temporary. A 4xx response means &#8220;try again later,&#8221; but a plain <code>sendMail()<\/code> call treats it as final and the message is lost.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5. Your domain has no authentication records<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Without SPF and DKIM aligned to your sending domain, mailbox providers treat your mail as unverified. Authentication failures are one of the most common causes of email delivery problems, and they produce no error in your application at all. Our guide to <a href=\"https:\/\/photonconsole.com\/blog\/spf-dkim-dmarc-explained-simply\/\">SPF, DKIM and DMARC explained simply<\/a> covers what to publish.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">6. You are sending through a personal mailbox<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Free Gmail accounts cap at roughly 500 recipients per day and Google Workspace accounts at 2,000, according to <a href=\"https:\/\/support.google.com\/a\/answer\/166852\" target=\"_blank\" rel=\"noopener\">Google&#8217;s published Workspace sending limits<\/a>. Application traffic hits these ceilings quickly, and exceeding them can suspend the account your team relies on.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step-by-Step: A Production Nodemailer Setup<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Install the package<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install nodemailer<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Choose the correct port<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Port choice determines whether your connection succeeds at all. The <code>secure<\/code> flag must match the port.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Port<\/th><th>Encryption<\/th><th>secure value<\/th><th>Use in production?<\/th><\/tr><\/thead><tbody><tr><td>587<\/td><td>STARTTLS (upgrades after connect)<\/td><td><code>false<\/code><\/td><td>Yes \u2014 recommended default<\/td><\/tr><tr><td>465<\/td><td>Implicit TLS (encrypted from start)<\/td><td><code>true<\/code><\/td><td>Yes \u2014 valid alternative<\/td><\/tr><tr><td>2525<\/td><td>STARTTLS<\/td><td><code>false<\/code><\/td><td>Yes \u2014 fallback when 587 is blocked<\/td><\/tr><tr><td>25<\/td><td>Usually none<\/td><td><code>false<\/code><\/td><td>No \u2014 blocked by most hosts<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Setting <code>secure: true<\/code> on port 587 is a frequent mistake and produces a connection that hangs rather than a clear error.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Store credentials in environment variables<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Never commit SMTP credentials. Put them in <code>.env<\/code> and add that file to <code>.gitignore<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SMTP_HOST=smtp.yourprovider.com\nSMTP_PORT=587\nSMTP_USER=your_smtp_username\nSMTP_PASS=your_smtp_password\nMAIL_FROM=\"Your App &lt;noreply@yourdomain.com&gt;\"<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4: Create one shared transporter<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Create this once, at module level, and import it wherever you send mail.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ mailer.js\nimport nodemailer from \"nodemailer\";\n\nexport const transporter = nodemailer.createTransport({\n  host: process.env.SMTP_HOST,\n  port: Number(process.env.SMTP_PORT),\n  secure: false,           \/\/ false for 587, true for 465\n  auth: {\n    user: process.env.SMTP_USER,\n    pass: process.env.SMTP_PASS,\n  },\n  pool: true,              \/\/ reuse connections\n  maxConnections: 5,\n  maxMessages: 100,\n  connectionTimeout: 10000,\n  greetingTimeout: 10000,\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/nodemailer.com\/smtp\" target=\"_blank\" rel=\"noopener\">official Nodemailer SMTP documentation<\/a> lists every available transport option.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 5: Verify the connection at startup<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This surfaces credential and firewall problems on deploy rather than on the first user signup.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>try {\n  await transporter.verify();\n  console.log(\"SMTP connection verified\");\n} catch (err) {\n  console.error(\"SMTP verification failed:\", err.message);\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 6: Send a message<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>export async function sendWelcomeEmail(to, name) {\n  const info = await transporter.sendMail({\n    from: process.env.MAIL_FROM,\n    to,\n    subject: \"Welcome aboard\",\n    text: `Hi ${name}, thanks for signing up.`,\n    html: `&lt;p&gt;Hi ${name}, thanks for signing up.&lt;\/p&gt;`,\n  });\n\n  console.log(\"Message sent:\", info.messageId);\n  return info;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Always include a <code>text<\/code> version alongside <code>html<\/code>. Messages with only an HTML body score worse with spam filters.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Quick Fix: Emails Send Locally but Not in Production<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Switch from port 25 to port 587<\/li>\n\n\n\n<li>Confirm <code>secure: false<\/code> is paired with port 587<\/li>\n\n\n\n<li>Check that environment variables are actually loaded on the server, not just in <code>.env<\/code><\/li>\n\n\n\n<li>Run <code>transporter.verify()<\/code> and read the error code<\/li>\n\n\n\n<li>Confirm your host has not firewalled outbound SMTP<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Step 7: Add connection pooling correctly<\/h3>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img fetchpriority=\"high\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/Why-Connection-Pooling-Matters-1024x576.png\" alt=\"\" class=\"wp-image-361\" style=\"width:697px;height:auto\" srcset=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/Why-Connection-Pooling-Matters-1024x576.png 1024w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/Why-Connection-Pooling-Matters-300x169.png 300w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/Why-Connection-Pooling-Matters-768x432.png 768w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/Why-Connection-Pooling-Matters-1536x864.png 1536w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/Why-Connection-Pooling-Matters.png 1672w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Pooling reuses open SMTP connections instead of performing a TLS handshake for every message.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Pooling keeps connections open between sends instead of performing a full TLS handshake each time. Nodemailer defaults to a maximum of 5 concurrent connections and 100 messages per connection, and it supports rate limiting through <code>rateDelta<\/code> and <code>rateLimit<\/code>. The <a href=\"https:\/\/nodemailer.com\/smtp\/pooled\" target=\"_blank\" rel=\"noopener\">pooled SMTP documentation<\/a> explains the full option set.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const transporter = nodemailer.createTransport({\n  \/\/ ...connection settings\n  pool: true,\n  maxConnections: 5,\n  maxMessages: 100,\n  rateDelta: 1000,   \/\/ time window in ms\n  rateLimit: 5,      \/\/ max messages per window\n});\n\n\/\/ Close the pool cleanly on shutdown\nprocess.on(\"SIGTERM\", () =&gt; {\n  transporter.close();\n  process.exit(0);\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Create the transporter once. Every call to <code>createTransport()<\/code> builds a separate pool, which defeats the purpose entirely.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 8: Handle errors and retry properly<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Distinguish permanent failures from temporary ones. Retrying a permanent failure wastes sends and damages your reputation; not retrying a temporary one loses the message.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>async function sendWithRetry(message, attempts = 3) {\n  for (let i = 0; i &lt; attempts; i++) {\n    try {\n      return await transporter.sendMail(message);\n    } catch (err) {\n      const permanent = err.responseCode &gt;= 500 &amp;&amp; err.responseCode &lt; 600;\n      if (permanent || i === attempts - 1) throw err;\n\n      const delay = Math.pow(2, i) * 1000;   \/\/ 1s, 2s, 4s\n      await new Promise((r) =&gt; setTimeout(r, delay));\n    }\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For the reasoning behind backoff intervals and when to stop retrying, see our guide to <a href=\"https:\/\/photonconsole.com\/blog\/smtp-retry-logic-explained-for-transactional-email-systems\/\">SMTP retry logic for transactional email systems<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 9: Move sending off the request path<\/h3>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/Queue-Your-Email-Sends-1024x576.png\" alt=\"\" class=\"wp-image-362\" style=\"aspect-ratio:1.7777777777777777;width:1200px;height:auto\" srcset=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/Queue-Your-Email-Sends-1024x576.png 1024w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/Queue-Your-Email-Sends-300x169.png 300w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/Queue-Your-Email-Sends-768x432.png 768w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/Queue-Your-Email-Sends-1536x864.png 1536w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/Queue-Your-Email-Sends.png 1672w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Queuing the send returns a response to the user immediately instead of waiting on an SMTP handshake.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Never make a user wait for an SMTP handshake. Push the message onto a queue and return the response immediately.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Instead of awaiting the send inside your route:\napp.post(\"\/signup\", async (req, res) =&gt; {\n  const user = await createUser(req.body);\n  await emailQueue.add(\"welcome\", { to: user.email, name: user.name });\n  res.status(201).json({ ok: true });\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is the single biggest reliability improvement in most Node.js applications. Our breakdown of <a href=\"https:\/\/photonconsole.com\/blog\/transactional-email-queue-architecture-explained\/\">transactional email queue architecture<\/a> covers worker design and failure handling.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Common Nodemailer Errors and What They Mean<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Error<\/th><th>Likely cause<\/th><th>Fix<\/th><\/tr><\/thead><tbody><tr><td><code>ECONNREFUSED<\/code><\/td><td>Wrong host or port, or port blocked<\/td><td>Switch to 587; confirm host spelling<\/td><\/tr><tr><td><code>ETIMEDOUT<\/code><\/td><td>Firewall silently dropping traffic<\/td><td>Check outbound rules; try port 2525<\/td><\/tr><tr><td><code>EAUTH<\/code><\/td><td>Invalid credentials or app password required<\/td><td>Regenerate credentials; enable 2FA app password<\/td><\/tr><tr><td><code>ESOCKET<\/code><\/td><td><code>secure<\/code> flag mismatched to port<\/td><td><code>false<\/code> for 587, <code>true<\/code> for 465<\/td><\/tr><tr><td><code>EENVELOPE<\/code><\/td><td>Malformed from or to address<\/td><td>Validate address format before sending<\/td><\/tr><tr><td><code>self signed certificate<\/code><\/td><td>TLS interception or local test server<\/td><td>Fix the certificate chain; do not disable TLS checks in production<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">For numeric SMTP replies rather than Node error codes, our reference on <a href=\"https:\/\/photonconsole.com\/blog\/smtp-response-codes-explained\/\">SMTP response codes explained<\/a> maps each code to its cause. Persistent authentication failures are covered in detail in our <a href=\"https:\/\/photonconsole.com\/blog\/smtp-authentication-error\/\">SMTP authentication error<\/a> guide, and hanging connections in <a href=\"https:\/\/photonconsole.com\/blog\/smtp-connection-timeout\/\">SMTP connection timeout<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Quick Fix: EAUTH Authentication Failed<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Enable two-factor authentication on the sending account<\/li>\n\n\n\n<li>Generate a dedicated app password rather than using the login password<\/li>\n\n\n\n<li>Remove spaces when pasting the generated password<\/li>\n\n\n\n<li>Confirm the username is the full email address where required<\/li>\n\n\n\n<li>Verify the credential belongs to the same host you are connecting to<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Platform-Specific Setup<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Express applications<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Define the transporter in a separate module and import it. Do not attach it to the request object or recreate it per route.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Next.js<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">SMTP cannot run in the browser or in the Edge runtime. Send only from Route Handlers, Server Actions, or API routes running on the Node.js runtime. Add <code>export const runtime = \"nodejs\"<\/code> to any route that sends mail.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Serverless functions and Lambda<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pooling provides little benefit when containers are short-lived, and open pools can hold a function open past its timeout. Set <code>pool: false<\/code>, keep timeouts below the platform limit, and prefer queue-triggered sends over synchronous ones.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Docker containers<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Minimal base images often lack CA certificates, which breaks TLS with a certificate error. Install <code>ca-certificates<\/code> in your image rather than disabling verification.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Shared hosting and cPanel<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Outbound SMTP is frequently restricted to the host&#8217;s own mail server. Port 2525 is often open when 587 is not; an external relay such as PhotonConsole avoids the restriction entirely.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Pro Tips for Production Email in Node.js<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Use a subdomain for sending.<\/strong> Sending from <code>mail.yourdomain.com<\/code> isolates application email reputation from your corporate mail.<\/li>\n\n\n\n<li><strong>Set a real reply-to address.<\/strong> Messages from a no-reply address with no reply path attract more complaints.<\/li>\n\n\n\n<li><strong>Log the messageId on every send.<\/strong> Without it you cannot trace a delivery complaint back to a specific message.<\/li>\n\n\n\n<li><strong>Test rendering before launch.<\/strong> Send a sample through <a href=\"https:\/\/www.mail-tester.com\/\" target=\"_blank\" rel=\"noopener\">mail-tester<\/a> to catch authentication and spam-score problems.<\/li>\n\n\n\n<li><strong>Verify DNS records after any change.<\/strong> <a href=\"https:\/\/mxtoolbox.com\/\" target=\"_blank\" rel=\"noopener\">MXToolbox<\/a> confirms SPF, DKIM and blocklist status.<\/li>\n\n\n\n<li><strong>Never disable TLS verification.<\/strong> <code>rejectUnauthorized: false<\/code> silences the symptom and leaves traffic exposed.<\/li>\n\n\n\n<li><strong>Separate transactional and bulk streams.<\/strong> A marketing send that damages reputation should never take password reset emails down with it.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">When to Move Beyond Gmail SMTP<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Gmail SMTP is acceptable for a prototype. It stops being viable once your application sends real volume.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mailbox providers cap daily sending, throttle automated traffic, and count every recipient individually. Beyond the raw limits, a mailbox gives you no delivery log, no bounce webhook, and no way to separate transactional traffic from your team&#8217;s day-to-day mail. The SMTP protocol itself, defined in <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc5321\" target=\"_blank\" rel=\"noopener\">RFC 5321<\/a>, has no concept of delivery reporting \u2014 that visibility has to come from the relay you send through.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A dedicated <a href=\"https:\/\/www.photonconsole.com\/relay.php\">SMTP relay service<\/a> such as PhotonConsole replaces the credentials in your existing Nodemailer config without any code change. You update three environment variables and gain delivery logs, bounce handling, authentication support and headroom that a mailbox cannot provide. Because pricing is pay-as-you-use, cost tracks actual send volume rather than a fixed tier \u2014 the <a href=\"https:\/\/www.photonconsole.com\/pricing.php\">pricing page<\/a> shows how that scales.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Configuring Nodemailer With PhotonConsole<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Setup is two steps: authenticate your domain in DNS, then point your transporter at the relay.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First, add the authentication records to your DNS provider so mailbox providers can verify your domain authorised the send:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>TXT    @                    v=spf1 include:relay.photonconsole.com ~all\nCNAME  photon._domainkey    dkim.photonconsole.com<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then update your environment variables and transporter:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .env\nSMTP_HOST=smtp.photonconsole.com\nSMTP_PORT=587\nSMTP_USER=your_project_api_user\nSMTP_PASS=your_secret_api_key<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>const transporter = nodemailer.createTransport({\n  host: 'smtp.photonconsole.com',\n  port: 587,        \/\/ 465 for implicit SSL, 2525 if 587 is blocked\n  secure: false,    \/\/ true only on port 465\n  auth: {\n    user: process.env.SMTP_USER,\n    pass: process.env.SMTP_PASS\n  },\n  pool: true,\n  maxConnections: 5\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Port 2525 matters here. It is the fallback for exactly the AWS, DigitalOcean and Azure port blocking described at the start of this guide, so a host that refuses 587 is no longer a dead end. Every account includes 5,000 emails per month at no cost, which is enough to validate the full setup \u2014 DNS records, pooling, retries and bounce webhooks \u2014 before any spend.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you are still evaluating options, our comparison of <a href=\"https:\/\/photonconsole.com\/blog\/free-smtp-servers\/\">free SMTP servers<\/a> covers where the free tiers stop being practical.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Related Issues You May Hit Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Mail sends successfully but never arrives.<\/strong> Delivery accepted by the server does not mean inbox placement \u2014 see <a href=\"https:\/\/photonconsole.com\/blog\/emails-sent-but-not-delivered\/\">emails sent but not delivered<\/a>.<\/li>\n\n\n\n<li><strong>Messages land in spam.<\/strong> Usually an authentication or reputation problem rather than content \u2014 see <a href=\"https:\/\/photonconsole.com\/blog\/why-emails-go-to-spam-in-gmail\/\">why emails go to spam in Gmail<\/a>.<\/li>\n\n\n\n<li><strong>Delivery is slow.<\/strong> Queue depth and provider throttling are the usual causes \u2014 see <a href=\"https:\/\/photonconsole.com\/blog\/emails-delayed\/\">emails delayed<\/a>.<\/li>\n\n\n\n<li><strong>Everything worked in dev and broke in prod.<\/strong> Our <a href=\"https:\/\/photonconsole.com\/blog\/transactional-emails-failing-in-production-but-working-in-dev-a-debugging-guide\/\">production debugging guide<\/a> covers the environment gaps.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently Asked Questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Is Nodemailer still maintained?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. Nodemailer remains the standard SMTP client for Node.js and is actively maintained. Current versions use <code>nodemailer.createTransport()<\/code> with an options object.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I use Nodemailer without an SMTP server?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. Nodemailer is a client and requires a mail server to relay through. Options are a personal mailbox, your own mail server, or a dedicated SMTP relay such as PhotonConsole.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Which port should I use?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Port 587 with <code>secure: false<\/code> for most cases. Use 465 with <code>secure: true<\/code> if your provider requires implicit TLS. Avoid port 25.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why does Gmail reject my password?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Google no longer accepts standard account passwords for SMTP. Enable two-factor authentication and generate an App Password, or use OAuth 2.0.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How many emails can Nodemailer send?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Nodemailer imposes no limit. Your ceiling is set entirely by the SMTP server you connect to.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Should I enable pooling?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes for long-running servers sending regularly. No for serverless functions, where containers are short-lived.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I test without emailing real users?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use a capture service that accepts messages without delivering them, or a dedicated test inbox. Never test against real customer addresses.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I still need SPF and DKIM if I use a relay?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. The relay transmits your mail, but the authentication records must exist on your own domain&#8217;s DNS for messages to be trusted.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Sending email from Node.js is straightforward. Sending email that reliably arrives is an infrastructure decision.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The setup above \u2014 port 587, environment-based credentials, a single pooled transporter, verified connections, classified retries, and queued sending \u2014 removes the failure modes that break most production applications. What it cannot do is fix the ceiling of the mail server underneath it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once your application sends OTPs, password resets or order confirmations, delivery stops being a developer convenience and becomes a business dependency. A failed password reset email is a support ticket; a failed OTP is a lost signup. At that point a purpose-built <a href=\"https:\/\/www.photonconsole.com\/\">email delivery service<\/a> like PhotonConsole, with logging, bounce handling and authentication support, is the correct foundation, and moving to one requires nothing more than changing the credentials your transporter already reads.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Read More<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-configuration\/\">SMTP Configuration: A Complete Setup Guide<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/email-api-integration\/\">Email API Integration for Developers<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-authentication-error\/\">How to Fix SMTP Authentication Errors<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-connection-timeout\/\">SMTP Connection Timeout: Causes and Fixes<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-retry-logic-explained-for-transactional-email-systems\/\">SMTP Retry Logic for Transactional Email<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/transactional-email-queue-architecture-explained\/\">Transactional Email Queue Architecture Explained<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/improve-email-deliverability\/\">How to Improve Email Deliverability<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/spf-dkim-dmarc-explained-simply\/\">SPF, DKIM and DMARC Explained Simply<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Your Node.js app sends email perfectly on localhost. You deploy it, and the emails stop arriving. No crash, no error in the logs, no bounce message. Just silence. This is the most common email failure in Node.js applications, and it is almost never a bug in your code. It is a configuration gap between your [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":360,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[312],"tags":[504,503,505,501,502],"class_list":["post-359","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-email-engineering-guide","tag-node-js-email-not-sending-in-production","tag-nodemailer-connection-pool","tag-nodemailer-eauth-error","tag-nodemailer-smtp-configuration","tag-send-email-node-js-nodemailer"],"_links":{"self":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/359","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/comments?post=359"}],"version-history":[{"count":1,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/359\/revisions"}],"predecessor-version":[{"id":363,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/359\/revisions\/363"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/media\/360"}],"wp:attachment":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/media?parent=359"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/categories?post=359"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/tags?post=359"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}