<!-- Markdown twin of https://pixelvault.dev/blog/host-images-in-resend-emails -->

[← Back to blog](/blog)

By [PixelVault](/about) July 15, 2026

# Hosting images in emails sent with Resend

Resend sends the email; it doesn't host your images. And the tempting shortcut — embedding the image as a base64 `data:` URI — is quietly stripped by Gmail and Outlook, so your recipients see a broken box. Here's the reliable pattern: put the image behind a permanent CDN URL, reference that URL from your HTML, and let it render everywhere.

You're sending a receipt, a weekly digest, or a campaign with [Resend](https://resend.com), and it has an image in it — a logo, a chart, a product shot. Everything looks perfect in your local preview. Then it lands in a real inbox and the image is a gray box with a torn-corner icon. This is the single most common email-image problem, and it comes down to one fact: **an email can only reliably show an image that lives at a public URL.**

## Why you can't just embed the image

There are three ways to get an image into an email, and two of them are traps:

-   **Base64 `data:` URI** — inlining the bytes directly in the `src`. It works in your browser preview, which is why people ship it. But **Gmail doesn't render `data:` images at all, and many Outlook clients don't either** — support is inconsistent, so a big slice of your list sees nothing. Don't rely on it.
-   **CID attachment** — attaching the image and referencing it with `cid:`. This renders more widely, but it bloats every single send with the full image payload, hurts deliverability, and Resend's HTML-first API isn't built around it.
-   **A hosted URL** — `<img src="https://…">`. This is what actually works everywhere, every time. The only question is _where that URL lives_.

So the real task isn't "embed an image in the email." It's "host the image somewhere public, then point the email at it." Resend deliberately leaves that half to you — it's an email API, not an image host.

## The two-step pattern

Host the image once to get a stable URL, then drop that URL into the HTML you hand to Resend. With PixelVault the hosting step is a single API call, and the URL you get back is permanent — which matters more for email than for almost anything else (more on that below). The upload runs **server-side** (it uses your secret `PIXELVAULT_API_KEY`, which must never ship to a browser) — the same place you call Resend from.

1\. Host the image 2\. Send with Resend

// Host the image once — permanent CDN URL back, zero egress.  
const form = new FormData();  
form.append("file", blob, "chart.png");  
  
const res = await fetch("https://api.pixelvault.dev/v1/images", {  
  method: "POST",  
  headers: { Authorization: \`Bearer ${process.env.PIXELVAULT\_API\_KEY}\` },  
  body: form,  
});  
// The URL is nested under \`data\` in the response envelope.  
const { url } = (await res.json()).data;  
// → https://img.pixelvault.dev/proj\_abc/img\_xyz.png

Do the hosting step wherever your images are born: when you generate the chart, when a designer drops a new banner into the repo, or once at build time for assets that don't change. The URL is stable, so a logo you host today keeps working in every email you ever send.

## Where the image lives matters more for email

Email is a uniquely punishing place to host images, for two reasons most hosting comparisons miss.

**Every open re-fetches the image.** Web pages get cached in one browser. An email image gets requested by every recipient, often re-fetched through client-side proxies (Gmail serves images through `googleusercontent.com`), across your whole list, for as long as the message sits in the inbox. That's the exact traffic pattern that runs up a metered egress bill on S3 + CloudFront — you're billed on views, and email is nothing but views. PixelVault stores on Cloudflare R2 with **zero egress fees**, so a campaign that gets opened a million times costs the same to serve as one that gets opened once.

**Inboxes live for years.** A receipt from 2026 might be opened in 2029. If the image URL breaks — bucket cleaned up, host shut down, link expired — the email breaks retroactively, and there's nothing you can do about a message already sitting in someone's archive. PixelVault URLs are **permanent and immutable** (`Cache-Control: public, max-age=31536000, immutable`), so the picture that renders today renders in three years.

## Where should the image live?

Base64 `data:`

S3 + CloudFront

PixelVault

Renders in Gmail/Outlook

Stripped — broken box

Yes

Yes

Email weight

Bloats every send

Light (a URL)

Light (a URL)

Setup

None, but unreliable

Bucket + CDN + IAM

One API call

Cost as opens scale

—

Metered egress

Zero egress

Still works in 2 years

n/a

If you keep the bucket

Permanent, immutable URL

## Sizing images for email

Email clients don't do responsive `srcset` well, so the convention is simple: serve a 2× image and constrain it with a plain `width` attribute. PixelVault resizes on the fly from transform params, so you host the original once and ask for the size you need at send time — `?w=1200` served, `width="600"` displayed, crisp on retina without shipping a separate export. Add `&fmt=auto` to let the CDN negotiate a modern format where the client supports it.

## One honest caveat

Hosting your images well makes them _render_ reliably and keeps your costs flat — it does **not** improve inbox placement or open rates. Deliverability is a function of your sending domain, SPF/DKIM/DMARC, and list hygiene: Resend gives you the domain-authentication tooling, but the DNS records and your sending practices are still yours to get right. Don't switch image hosts expecting to land in more inboxes. Switch because your images stop breaking and your egress bill stops scaling with your success.

## Free to start

PixelVault's free tier includes 200 MB storage, 500 uploads/month, and 1 GB bandwidth — no credit card, zero egress. Plenty to host the images for a transactional flow or a modest campaign list before you pay anything. Paid plans start at [$9/month](/pricing).

There's a copy-paste helper for the `react-email` stack in the [docs](/docs#email), or start from the [API quickstart →](/docs)
