Desktop Pricing Docs Blog About GitHub View as Markdown (Agent) Get started
Human Agent
← Back to blog

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, 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.

// 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.

There's a copy-paste helper for the react-email stack in the docs, or start from the API quickstart →