---
title: How to send emails from an application on Vercel
description: Send email from Vercel Functions over an HTTP API instead of SMTP. Match the right Next.js pattern to your trigger and fix sends that fail in production.
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: 2026-09-09
last_updated: 2026-09-09
authors: Ben Sabic
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/workflow
  - /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
---

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:

| Failure mode         | What happens                                                                                                                                                                                                                                                            | Where it applies                                                                                                                                              |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Missing `await`      | The function returns its response before the SMTP exchange finishes, and the connection drops mid-sequence. Nothing throws, so your logs stay clean while the message never leaves.                                                                                     | Every runtime and every plan                                                                                                                                  |
| Short duration limit | The SMTP handshake stacks on top of cold start latency, and Vercel terminates the function before the exchange finishes. The caller sees a 504 with the `FUNCTION_INVOCATION_TIMEOUT` error code, and the SMTP client may also log a socket error such as `ECONNRESET`. | Projects running without [Fluid compute](https://vercel.com/docs/fluid-compute), where the legacy defaults of 10 seconds on Hobby and 15 seconds on Pro apply |
| 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.             | Only functions you've configured for the Edge runtime. Node.js is the default.                                                                                |

The first failure mode is the most common because it produces no error. The two lines below differ by one keyword, and only the second one sends the message:

```typescript
// Drops the connection: the response returns before the SMTP exchange completes
transporter.sendMail(message);
return NextResponse.json({ sent: true });

// Completes the exchange: the response waits for the provider
await transporter.sendMail(message);
return NextResponse.json({ sent: true });
```

The duration limit matters less than it used to. Functions run on Fluid compute by default, which gives every plan a default maximum duration of 300 seconds. Pro and Enterprise teams can raise that to 800 seconds, or opt into an extended 1800-second maximum in beta on supported Node.js, Bun, and Python runtime versions. The 10-second and 15-second defaults now apply only to projects that have Fluid compute disabled.

Vercel blocks port 25 for outgoing connections, while ports 465 and 587 stay open. An open port doesn't remove 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

Sending over a provider's HTTP endpoint avoids all three failure modes because the send requires a single request rather than 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 occurs after the function initializes and before any message data is transferred. 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.

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 can be integrated into 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 or Google Workspace account, with more providers coming soon. 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.          |

Marketplace integrations save 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 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 | Forms, event handlers, or transitions 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 via POST and are callable directly from React components, which is well-suited to contact forms and signup confirmations. Return errors as part of the response object rather than throwing them, so the caller gets a value they can render. Keep provider credentials on the server and return a generic message to the client to keep provider details 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:

| API           | Import from         | Use it when                                                                                                                                                                                                              |
| ------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `after()`     | `next/server`       | Your project runs Next.js 15.1 or later. Stable since 15.1 and available in Server Components, Server Actions, Route Handlers, and Middleware.                                                                           |
| `waitUntil()` | `@vercel/functions` | You're outside Next.js or below 15.1. 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). |

This Route Handler responds immediately and schedules the send with `after()`:

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

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

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

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

    if (error) {
      console.error('Email error:', error);
    }
  });

  return NextResponse.json({ accepted: true }, { status: 202 });
}
```

The caller no longer sees the send result, so log the provider's error field inside the callback. Neither API escapes your function's duration limit. Promises passed to `waitUntil()` share the function's timeout and are 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), available in public beta on all plans, persists the message with at-least-once delivery, retries failed processing, and names deferring email as one of its use cases. Your route publishes and returns, and a consumer function performs the send. Adding a queue trigger makes the consumer route private, so only Vercel's queue infrastructure can invoke it.

For multi-step flows around the send, such as waiting for a confirmation event before a follow-up email, [Vercel Workflows](https://vercel.com/docs/workflow) builds on Queues and lets a function pause and resume across deployments.

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:

| Symptom                                                                  | Cause                                                     | Fix                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ------------------------------------------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The provider call fails only after you promote                           | 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). Set the key 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.                                                 |
| Anyone can send from your domain                                         | `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. Drop the prefix and read the key on the server only.                                                                                                                                                                                                                             |
| Your route returns 200 but nothing reaches the inbox                     | Missing `await` on the send                               | The signature of this one is a successful response paired with an absent send. Await every provider call and check the `error` field it returns.                                                                                                                                                                                                                                                                    |
| Incoming mail stops while sending keeps working                          | Mail exchange (MX) records lost after a nameserver change | Pointing your nameservers to Vercel doesn't carry over your existing mail records, and Vercel doesn't provide its own mail service. 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.                                                                                                                            |
| The API key is `undefined` at runtime, or the browser blocks the request | Sending from a Client Component                           | Client Components can't read server-side environment variables, and a Content Security Policy can block the browser's outbound request. Keep every send in a Server Action, Route Handler, or API Route.                                                                                                                                                                                                            |
| Every send fails against a daily cap                                     | No throttling on the send endpoint                        | An unthrottled route lets one caller burn your provider quota. Add a rate limit rule in your project's Firewall settings, then call `checkRateLimit` from `@vercel/firewall` in the route, as shown in the [Rate Limiting SDK](https://vercel.com/docs/vercel-firewall/vercel-waf/rate-limiting-sdk) docs. 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 a deployment to Production:

1. Check the API response: Successful Resend sends return `data.id`. Log it or return it from your handler to confirm the call reached the provider. When `id` is missing, the call never reached the provider.
   
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.
   

Preview deployments mirror 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)
  
- [Vercel Queues](https://vercel.com/docs/queues)
  
- [Vercel Workflows](https://vercel.com/docs/workflow)
  
- [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. Keys added to Preview alone aren'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, now in public beta on all plans, when the send has to survive a crash, a timeout, or a deployment rollout, or Vercel Workflows when the email is one step in a longer durable flow.