---
title: Sending Emails from an application on Vercel
description: SMTP is the harder path inside Vercel Functions. Learn how to send emails over an HTTP API, which Next.js pattern fits your trigger, and how to fix failed sends.
url: /kb/guide/sending-emails-from-an-application-on-vercel
canonical_url: "https://vercel.com/kb/guide/sending-emails-from-an-application-on-vercel"
published: 2025-11-03
last_updated: 2026-08-18
authors: Rich Haines
related:
  - /docs/functions
  - /docs/fluid-compute
  - /docs/functions/runtimes/edge
  - /kb/guide/serverless-functions-and-smtp
  - /docs/functions/functions-api-reference/vercel-functions-package
  - /docs/queues
  - /docs/environment-variables
  - /docs/environment-variables/sensitive-environment-variables
  - /docs/domains/managing-dns-records
  - /docs/vercel-firewall/vercel-waf/rate-limiting-sdk
  - /docs/functions/limitations
  - /kb/guide/using-email-with-your-vercel-domain
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---
<!-- docsgraph:related -->
## Related pages

> **For AI agents:** Follow these links to understand how this page connects to the rest of the Vercel ecosystem. For the full cross-link map (inbound, outbound, prerequisites, and semantic neighbors), see the .graph.md link below.

- [Concepts](https://vercel.com/docs/queues/concepts?from=related) — Learn delivery, retries, visibility timeouts, and deployment isolation in Vercel Queues.
- [Quickstart](https://vercel.com/docs/queues/quickstart?from=related) — Set up Vercel Queues with the SDK.
- [Vercel Deployment Guide](https://ai-sdk.dev/docs/advanced/vercel-deployment-guide?from=related)
- [How to ship an Express app on Vercel](https://vercel.com/kb/guide/ship-a-express-app-on-vercel?from=related) — Deploy an Express app to Vercel with zero configuration. Configure response streaming, middleware, cron jobs, the Bun ru
- [How to enable CORS on Vercel](https://vercel.com/kb/guide/how-to-enable-cors?from=related) — Learn how to enable CORS on Vercel with vercel.json, Routing Middleware, framework config, and route handlers, plus how
- [Deploying React with Vercel](https://vercel.com/kb/guide/deploying-react-with-vercel?from=related) — Deploy React with Vercel to replace your build pipeline and shared staging. See how framework detection, previews, and F
- [Publish and subscribe to realtime data on Vercel](https://vercel.com/kb/guide/publish-and-subscribe-to-realtime-data-on-vercel?from=related) — Learn how to publish and subscribe to realtime data on Vercel with WebSockets, SSE, Redis, and Queues, and when a manage
- [Migrate a Next.js app from Webflow Cloud to Vercel](https://vercel.com/kb/guide/migrate-a-next-js-app-from-webflow-cloud-to-vercel?from=related) — Move your Next.js app from Webflow Cloud to Vercel: remove the OpenNext Cloudflare adapter, drop the base path, map stor

Full cross-link map for this page: [/kb/guide/sending-emails-from-an-application-on-vercel.graph.md](/kb/guide/sending-emails-from-an-application-on-vercel.graph.md)
<!-- /docsgraph:related -->


You can send emails from your application with [Vercel Functions](https://vercel.com/docs/functions), either over an outgoing Simple Mail Transfer Protocol (SMTP) connection or through a third-party provider's HTTP API. The HTTP API is the more reliable of the two, and the reason comes down to how an SMTP connection behaves inside a function that stops running once it responds.

Here's why SMTP struggles on Vercel, what to use instead, and how to fix sends that fail after you deploy.

## **Why does SMTP fail when you send emails from Vercel?**

SMTP looks like a safe default because it works on any server that runs continuously. Vercel Functions don't. Once a function returns its HTTP response, work still in progress is paused and may not resume when the function is next invoked.

Every SMTP send depends on a single stateful connection over Transmission Control Protocol (TCP). That connection works through an ordered exchange of Domain Name System (DNS) lookup, TCP handshake, Transport Layer Security (TLS) negotiation, authentication, message transfer, and disconnect. Each step needs the previous one to finish on the same socket, so the entire exchange has to complete before your function responds.

Three failure modes follow from that:

- **A missing** `**await**`**The function returns its response before the SMTP exchange finishes, which causes the connection to drop** mid-sequence. Nothing throws, so your logs stay clean while the message never leaves. This one applies on every runtime and every plan.
  
- **A duration limit without Fluid compute:** The SMTP handshake stacks on top of cold start latency, and the connection can reset with an `ECONNRESET` before TLS finishes. Functions run on [Fluid compute](https://vercel.com/docs/fluid-compute) by default, which gives Hobby projects 300 seconds. Disabling it reverts the default to 10 seconds on Hobby and 15 seconds on Pro, which is where this failure occurs.
  
- **The Edge runtime:** No SMTP library runs there, so the send fails before a connection is even attempted. The [Edge runtime](https://vercel.com/docs/functions/runtimes/edge) exposes a small subset of Node.js modules, and `net` isn't among them, which means no TCP sockets. Node.js is the default runtime, so this applies only to functions you've configured for the Edge runtime.
  

Vercel blocks port 25 for outgoing connections, while ports 465 and 587 stay open. An open port doesn't remove the duration limit or the connection lifecycle above, so SMTP remains the harder path. For more on that path specifically, see [SMTP on Vercel](https://vercel.com/kb/guide/serverless-functions-and-smtp). Most mail providers offer an HTTP path alongside SMTP, and it suits this execution model far better.

## How to send email from a Vercel Function over HTTP

A send over a provider's HTTP endpoint avoids all three failure modes because it needs one request instead of a persistent connection.

That request is stateless and completes in a single round trip. There's no connection pool to manage, no socket to keep alive, and no port restriction to work around. The same code runs on every runtime Vercel supports, including the Edge runtime.

Cold starts show the difference clearly. With SMTP, the TCP and TLS handshake runs after the function initializes and before any message data moves. With an HTTP API, the send is one awaited `fetch` call. Fluid compute reduces how often you pay a cold start at all, since it reuses warm instances before creating new ones.

The pattern is the same for every provider, so the next decision is which one to use.

## How to choose an email provider that works with Vercel

Any provider with an HTTP API works inside a Vercel Function. Some of them install from the [Vercel Marketplace](https://vercel.com/marketplace) and write their credentials into your project's environment variables during setup, which removes a manual step and a common source of failure.

These providers send over HTTP from Vercel Functions:

| Provider                                            | Marketplace integration | Notes                                                                                                                                |
| --------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| [Resend](https://vercel.com/marketplace/resend)     | Yes                     | Adds `RESEND_API_KEY` to your project during install. HTTP-only API with React Email support.                                        |
| [SendWith](https://vercel.com/marketplace/sendwith) | Yes                     | Sends through an existing Gmail, Workspace, or Outlook account. Adds its API key to your environment variables.                      |
| SendGrid                                            | No                      | Offers an HTTP API alongside SMTP. Use the HTTP path on Vercel.                                                                      |
| Postmark                                            | No                      | Publishes official client libraries for API-based sending.                                                                           |
| AWS SES                                             | No                      | Sends over the SES v2 API. Move your account out of the sandbox before sending to unverified addresses.                              |
| Mailchimp                                           | No                      | Transactional sends run through Mailchimp Transactional, a paid add-on that requires a Standard or Premium plan and its own API key. |

A Marketplace integration saves setup time, though a provider without one works the same way once you add its API key. The Resend client also accepts a React component through its `react` parameter, which turns templates into typed components you can render and preview locally instead of interpolated HTML strings. With a provider chosen, the remaining decision is where in your app the send lives.

## Which Next.js pattern fits your email trigger?

The trigger decides the pattern. Sends that start from a user action inside your app belong in a Server Action, and sends that start from an external caller belong in a Route Handler. Pages Router projects use API Routes for the same job.

Match the trigger to the pattern:

| Pattern                   | Use it for                                                                   | Invoked by                                          |
| ------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------- |
| Server Actions            | User actions inside your app, such as contact forms and signup confirmations | A form, event handler, or transition in your own UI |
| Route Handlers            | External callers, such as webhooks, mobile apps, and third-party services    | Any HTTP client                                     |
| API Routes (Pages Router) | The same cases as Route Handlers                                             | Any HTTP client                                     |

All three are public HTTP endpoints. Server Actions are [publicly accessible HTTP endpoints](https://nextjs.org/docs/app/guides/data-security) even though you call them like functions, so authenticate the caller, check authorization, and validate input inside the action itself. Rendering a form only on an authenticated page isn't a security boundary.

### Server Actions for form-triggered email

Server Actions run on the server over POST and are callable directly from React components, which suits contact forms and signup confirmations. Return errors as part of the response object rather than throwing, so the caller gets a value it can render. Keep provider credentials on the server and return a generic message to the client so provider details stay server-side.

This Server Action sends a welcome email and returns the result to the caller:

```tsx
'use server';

import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

export async function sendWelcomeEmail(email: string, username: string) {
  try {
    const { data, error } = await resend.emails.send({
      from: 'Your App <onboarding@yourdomain.com>',
      to: email,
      subject: `Welcome to Your App, ${username}`,
      html: `<p>Welcome ${username}</p>`,
    });

    if (error) {
      console.error('Email error:', error);
      return { success: false, error: error.message };
    }

    return { success: true, id: data?.id };
  } catch (error) {
    console.error('Unexpected error:', error);
    return { success: false, error: 'A system error has occurred' };
  }
}
```

The returned `id` confirms the provider accepted the message. For callers outside your app, use a Route Handler instead.

### Route Handlers for webhook-triggered email

When a payment provider posts a webhook or a mobile client calls your endpoint, you need a Route Handler. Route Handlers use the Web `Request` and `Response` APIs and accept requests from any HTTP client.

The rule from the Server Action still applies. Await the send, check the provider's error field, and return a response on every code path.

This Route Handler accepts a POST body and sends the email:

```tsx
import { NextResponse } from 'next/server';
import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(request: Request) {
  try {
    const { email, subject, html } = await request.json();

    const { data, error } = await resend.emails.send({
      from: 'Your App <onboarding@yourdomain.com>',
      to: email,
      subject,
      html,
    });

    if (error) {
      return NextResponse.json({ error: error.message }, { status: 500 });
    }

    return NextResponse.json({ id: data?.id }, { status: 200 });
  } catch (error) {
    console.error('Email error:', error);
    return NextResponse.json({ error: 'Failed to send email' }, { status: 500 });
  }
}
```

Both patterns hold the response open until the provider replies. When that latency matters, move the send off the response path.

## How to send email without blocking your Vercel Function response

Awaiting the send inside the request is the right default for transactional email. The provider call is a single HTTP round trip, and handling it inline keeps success and failure in one place.

When you'd rather respond first, Next.js and Vercel each provide a way to continue work after the response is sent:

- `**after()**` **from** `**next/server**`**:** Schedules a side effect that runs after the response is sent. Use this on Next.js 15.1 and later.
  
- `**waitUntil()**` **from** `**@vercel/functions**`**:** Extends the request lifecycle around a promise, as described in the [functions API reference](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package). Use it outside Next.js or below 15.1.
  

Neither one escapes your function's duration limit. A promise passed to `waitUntil()` shares the function's timeout and is cancelled if the function times out, so a slow provider can still lose the send.

For sends that need to survive a crash, a timeout, or a deployment rollout, publish a message rather than sending inline. [Vercel Queues](https://vercel.com/docs/queues) persists the message, retries delivery, and names deferring email as one of its use cases. Your route publishes and returns, and a consumer function performs the send.

Most sends that fail after you deploy trace back to configuration rather than to either of these choices.

## How to troubleshoot email that fails to send in production

Work through these six causes:

- **Credentials missing in Production:** Local `.env` files aren't read in production, and environment variables are [scoped per environment](https://vercel.com/docs/environment-variables) across Production, Preview, and Development. A key set for Preview alone fails the moment you promote. Set it for Production in your project settings, and mark it a [sensitive environment variable](https://vercel.com/docs/environment-variables/sensitive-environment-variables) so nobody can read it back.
  
- **A** `**NEXT_PUBLIC_**` **prefix on a credential:** Any variable with this prefix is inlined into the client bundle at build time, which makes it readable in browser developer tools. An email API key exposed that way lets anyone send from your domain. Drop the prefix and read the key on the server only.
  
- **A missing** `**await**` **on the send:** Your route returns 200 while nothing appears in your provider's dashboard. A successful response paired with an absent send is the signature of this one. Await every provider call and check the `error` field it returns.
  
- **Mail exchange (MX) records after a nameserver change:** Pointing your nameservers at Vercel doesn't carry over your existing mail records, and Vercel doesn't provide a mail service of its own. Incoming mail stops while sending keeps working. Add the MX records your provider requires, or apply a [DNS Preset](https://vercel.com/docs/domains/managing-dns-records) if your provider is listed.
  
- **Sending from a Client Component:** Client Components can't read server-side environment variables, so the API key is either undefined at runtime or exposed in the bundle. A Content Security Policy can also block the outbound request from the browser. Keep every send in a Server Action, Route Handler, or API Route.
  
- **No throttling on the send endpoint:** An unthrottled route lets one caller burn your provider quota, and every send afterward fails against the daily cap. Add a rate limit rule in your project's Firewall settings, then call `checkRateLimit` from the route with the [Rate Limiting SDK](https://vercel.com/docs/vercel-firewall/vercel-waf/rate-limiting-sdk). Rate limiting is available on every plan, with the first 1,000,000 allowed requests included each month.
  

Working through these in order usually surfaces the failure before you need to read provider logs, since the first three account for most of them. Confirming each one ahead of a deploy is faster than diagnosing it afterward.

### How to verify your email setup before you deploy

These four checks confirm each cause above is handled before you promote to Production:

1. **Check the API response:** A successful Resend send returns [`data.id`](http://data.id). Log it or return it from your handler to confirm the call reached the provider. A missing `id` means it didn't.
   
2. **Confirm the environment scope:** Open your project settings and verify the API key is set for Production, not only for Preview.
   
3. **Read the provider dashboard:** Delivery status, bounces, and failures live with your provider. Treat that as the source of truth for whether the message left.
   
4. **Smoke test in Preview:** Deploy to a preview branch and trigger a real send before you merge.
   

A preview deployment mirrors your production configuration, so credential and scope problems surface there before they reach production traffic.

## Next steps

With an HTTP provider and the right Next.js pattern in place, your sends are ready to deploy. [Start a new Vercel project](https://vercel.com/new) to wire one up, or [browse the templates](https://vercel.com/templates) for a starting point that already includes email.

## Related resources

- [Vercel Functions](https://vercel.com/docs/functions)
  
- [SMTP on Vercel](https://vercel.com/kb/guide/serverless-functions-and-smtp)
  
- [Vercel Functions limits](https://vercel.com/docs/functions/limitations)
  
- [`waitUntil`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package) [reference](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package)
  
- [Environment variables](https://vercel.com/docs/environment-variables)
  
- [Email with Vercel domains](https://vercel.com/kb/guide/using-email-with-your-vercel-domain)
  

## Frequently asked questions

### Can I use Nodemailer on Vercel?

Yes, with caveats. Nodemailer runs in the Node.js runtime on ports 465 or 587, using a host value from a third-party SMTP service, and you have to await the send. It doesn't run in the Edge runtime, which has no `net` module. For production traffic, an HTTP API is the more reliable choice.

### Can I use an apex domain on Vercel and still receive email?

Yes, as long as you point the apex at Vercel with an A record rather than a CNAME. The DNS specification forbids other records alongside a CNAME, so a CNAME at the apex would displace your NS and MX records. Forwarding services such as ImprovMX work once their records are in place.

### Why does my email send work in Preview but fail in Production?

Environment variables on Vercel are scoped per environment. A key added to Preview alone isn't available in Production, so the provider call fails once you promote. Open your project settings, confirm the variable is set for Production, then redeploy. Variable changes apply to new deployments rather than existing ones.

### Do I need a queue to send transactional email from Vercel?

For most request-scoped sends, no. Awaiting the provider call inside your Server Action or Route Handler is sufficient. Use `after()` or `waitUntil()` when you want to respond before the send finishes. Reach for Vercel Queues when the send has to survive a crash, a timeout, or a deployment rollout.