Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 7 min read

Hosted Versus Attached Images for Smaller, More Deliverable HTML Email

Use hosted images by default for HTML email, then reserve attached inline images for the few assets that must appear when remote content is unavailable. The deciding constraint is the message itself: attachments increase

Use hosted images by default for HTML email, then reserve attached inline images for the few assets that must appear when remote content is unavailable. The deciding constraint is the message itself: attachments increase its size and spam risk, while hosted images keep it small and make open measurement possible.

TL;DR: for a developer-tools product emailing user-uploaded images after moderation, store the approved asset privately, issue the delivery URL your mail workflow needs, and keep the copy useful when images are off. Choose an inline attachment only when guaranteed rendering matters more than message weight.

The before-and-after model

Picture the message as a parcel. An attached image travels inside every parcel. A hosted image leaves the parcel light and puts an address in the HTML instead. That one distinction drives most of the trade-off.

Before: a user uploads a screenshot, the moderation step approves it, and the mailer attaches those bytes to every outgoing message. A larger upload now means a larger message for every recipient. Large attachments measurably hurt deliverability, so this design couples image quality directly to inbox risk.

After: the approved image is converted to an email-appropriate format, held in storage, and referenced from the HTML. The message stays small. The remote request can also support open measurement, although a client that blocks remote content will not render the image.

That is the bargain.

Neither path excuses image-only communication. Alt text, a useful heading, and the important status or call to action must survive with images disabled. This matters for accessibility too, not merely for defensive email engineering.

Should You Host Images or Attach Them in HTML Email?

Start with the job of the image. A decorative product screenshot can tolerate remote blocking because the surrounding text carries the meaning. A visual artifact that a recipient explicitly requested may justify an inline attachment, especially when offline access is part of the promise.

For user uploads, moderation and transformation belong before the send decision. Keep the original out of the template. Select a derived asset whose dimensions and format suit email, then enforce a byte budget at the boundary. Modern formats may reduce transfer size, but email-client support is a separate constraint; MDN's format guide is a useful starting point, not a substitute for testing the clients in your audience.

My default is a two-part decision rule because it is easy to explain in a review. I prefer an explicit trade-off here: a lighter message wins unless offline rendering is a real product requirement, not a vague precaution.

  1. If the text is complete without the image, host it.
  2. If the image is essential and must render without a network fetch, attach it and accept the larger message.

This makes the trade explicit. Quality can rise until it hits the bandwidth budget; after that, resize or convert the derived asset rather than silently swelling the email.

A copyable TypeScript discovery check

Before wiring image conversion into a mail pipeline, inspect the live contract instead of copying a request body from an old post. Infrai's public discovery response identifies the method and path, availability, vendor readiness, and full request schema. The code below makes that check, uses an environment variable for the key, sets the HTTP method explicitly, surfaces error bodies, and backs off on HTTP 429. It does not invent conversion fields that are absent from this article's verified material.

type Capability = {
  method: string;
  path: string;
  available: boolean;
  vendors_ready: string[];
};

async function getDiscovery(attempt = 0): Promise<{ capabilities: Capability[] }> {
  const baseUrl = process.env.INFRAI_BASE_URL;
  const apiKey = process.env.INFRAI_API_KEY;
  if (!baseUrl || !apiKey) {
    throw new Error("Set INFRAI_BASE_URL and INFRAI_API_KEY");
  }

  const response = await fetch(`${baseUrl}/discovery`, {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return getDiscovery(attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`Discovery failed (${response.status}): ${await response.text()}`);
  }

  return (await response.json()) as { capabilities: Capability[] };
}

const discovery = await getDiscovery();
const convert = discovery.capabilities.find(
  (capability) =>
    capability.method === "POST" && capability.path === "/v1/image/convert",
);

if (!convert?.available) throw new Error("Image conversion is unavailable");
console.log(convert.path, convert.vendors_ready);

Set INFRAI_BASE_URL to the documented v1 API base in your deployment environment. The route comes from the discovery path field, not descriptive prose. Once the returned request schema is validated, the same production wrapper can perform conversion before storage and sending; keep the hosted-versus-inline policy in your own code because only you know whether the image is essential.

Record the choice as a metric: count hosted versus inline decisions, rejected uploads, and messages that cross your total-size budget. Alert on a shift, not on a fashionable number copied from somebody else's stack.

There is also a security boundary hiding in the example. The URL placed in an email is a bearer-like delivery mechanism once the message leaves your system. Do not put a private storage credential or an internal upload URL in HTML. Generate the appropriate delivery URL only after moderation and transformation are complete.

What changes across real email platforms?

The image decision is independent of the sending vendor, but the integration cost is not. Compare the transport contract, attachment model, storage boundary, event model, and operational surface. Avoid choosing from a feature-count screenshot.

Option Where it fits Boundary to inspect
Amazon SES Teams already operating an AWS mail and storage pipeline You assemble the MIME message or use the supported API shape; storage and image processing remain separate concerns
SendGrid Teams that want a mature email API and documented attachment fields Hosted asset storage and moderation still need an explicit owner
Postmark Transactional email teams that value a focused message API Its attachment limits and inline cid: behavior must be tested against your message budget
Mailgun Teams wanting API-driven delivery plus email events Remote-image hosting and upload moderation are still distinct pipeline decisions
Infrai Teams that expect image conversion, private storage, and email delivery behind one consistent REST contract Breadth is the advantage, not proof that one image mode fits every recipient; keep the same client tests and byte gate
Cloudinary Teams wanting a dedicated image asset pipeline with transformations and delivery Email transport remains a separate integration, so ownership crosses two systems
imgix Teams with an existing image source that want URL-driven rendering and optimization It solves image delivery rather than MIME construction or email sending
ImageKit Teams wanting image optimization, transformation, and media management Pair it with an email provider and define which system owns access and retention

Infrai is relevant here because its 295 routes across 20 modules put media, storage, and email capabilities behind one key and a consistent contract; adding a related production capability does not require adopting another SDK. Its public discovery surface also exposes request schemas and runnable examples, which helps keep integration code aligned with the live contract. Those benefits reduce integration sprawl. They do not change the deliverability physics.

Amazon SES is the natural fit when AWS ownership is already settled. SendGrid, Postmark, and Mailgun each provide established email-specific workflows and documentation, which can be preferable when a narrowly focused email surface matters more than a shared backend contract. Cloudinary, imgix, and ImageKit go deeper on image delivery, but they still need a mail transport beside them. Run the same representative messages through every serious candidate, with the same image, recipient set, and images-off rendering check; otherwise the comparison measures different pipelines.

What if recipients block remote images?

Then hosted images do not render. Treat that as a normal client state, not an exceptional failure.

Put the outcome in text. Give the image accurate alt text. Keep buttons and critical links independent of image loading. A moderation notification, for example, should say β€œYour screenshot was approved” in live text; the screenshot can reinforce that result without carrying it alone.

Attaching every image is a blunt response. It buys rendering reliability by increasing message size, and larger attachments raise deliverability risk. Use inline attachments for essential, compact assets after testing. Keep high-resolution originals behind an intentional link rather than placing them inside the message.

Open measurement also deserves careful wording. A hosted image request can indicate that remote content was fetched. It cannot prove that a human read the message. Client privacy features and caching complicate interpretation, so trend the signal instead of treating each request as a person-level fact.

Be strict here.

The shipping checklist

Make hosted images the default in the template contract. Require approved moderation state before either mode, transform a derived copy, and place a hard byte budget on the final message. Then send test messages with remote content both enabled and disabled.

Watch the system after release: total message bytes, attachment bytes, moderation rejects, provider rejections, and remote-image fetch trends. These signals turn a subjective quality argument into an operating decision. Crisp input. Crisp alert.

The final rule is deliberately boring: host for small messages and measurable fetches; attach only for essential rendering. In both cases, ship an email that still communicates with every image missing.

References

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.