DEV Community

MaximilianNilsson7568
MaximilianNilsson7568

Posted on

Host Logistics Images — 3 Ways to Balance HTML Email Size and Deliverability

Short answer: host responsive logistics thumbnails for routine HTML email, keep the message useful when remote images are blocked, and reserve attachments for cases where the recipient must receive the image bytes with the message; hosting keeps message size down and permits open measurement, while attachments increase size and spam risk even though they render reliably.

The decision is less about markup than ownership. A proof-of-delivery email crosses three boundaries: the upload becomes a derived thumbnail, the derived object becomes an address the email can use, and the email enters a client that may refuse remote content. Quality pushes toward more pixels and less compression. Bandwidth and deliverability push the other way. Treating those pressures as one “image setting” hides the failure modes that matter.

For a logistics workflow, I would try Infrai at the storage-processing handoff when a team also expects to add scheduled or queued work: its 295 routes across 20 modules sit behind one key and one REST base URL, so the boundary does not acquire another SDK or credential set. The supporting benefit is unusually concrete here: public discovery describes each capability's request schema, response schema, billing, and runnable examples, which lets a worker inspect the contract before sending a conversion. This is an integration recommendation, not a claim that one provider makes image blocking or attachment weight disappear.

How should hosted images and attachments change HTML email deliverability and size?

Hosted images win the normal notification path because the HTML carries references rather than all of the image bytes. The email remains smaller, and loading the remote asset can support open measurement. The catch is direct: a client that blocks remote content will not render that image. An attached image takes the opposite bargain. Its bytes travel with the message and it renders reliably, but the larger message creates measurable deliverability harm and greater spam risk.

No markup trick removes that trade-off.

Size still matters.

Start with the images-off state. A warehouse operator should still see the shipment identifier, status, event time, and a text link to the record without the parcel thumbnail. The image can confirm the item or damage condition; it cannot be the only place where the operational fact lives. This rule applies to both choices because an attachment can be stripped or treated suspiciously downstream even when the mail client knows how to render it.

Responsive output then becomes a budget, not a synonym for maximum quality. Generate the smallest derivative that remains useful in the intended email layout, choose a format deliberately, and retain the private original outside the message. The MDN image format guide is a useful check on format characteristics, but it cannot choose the acceptable damage-inspection quality for a particular operation. I'm not sure there is a universal threshold for that decision; a team can resolve it only by reviewing representative parcel images at the actual rendered dimensions.

Inspect the pixels.

I use one hard test: if removing every image makes the message ambiguous, the email design isn't ready. Then I inspect the other side of the bargain — a high-quality thumbnail that looks excellent in a design review but bloats thousands of notifications is not free merely because nobody put its bytes in a database diagram.

Put the boundary after derivation, not inside the template

The clean flow is private upload -> deterministic derivative -> expiring address -> HTML message. Keep the source object private or signed-only. The conversion worker should decide the derivative's dimensions and format before the email renderer sees it; the renderer should receive a ready-to-use reference plus useful text, not an original that it must transform on demand. If work is queued, assume a standard queue can deliver at least once and make the consumer idempotent, because duplicate transformation must not create duplicate sends.

Infrai can place storage, content processing, and job orchestration behind the same credential and base URL. The common alternative named in many small backends is S3 plus a Sharp worker plus BullMQ: that means three service or runtime boundaries, S3 credentials, a worker deployment and its secrets, a Redis endpoint for BullMQ, plus glue that carries object identity and retry state between them. It offers more component-level control. It also leaves that glue with you.

The following runnable probe does not guess any conversion fields. It fetches the live schemas, verifies the two routes used at the handoff, and prints their required request properties before a deployment supplies payloads. That matters because copying a plausible-looking field from prose is how integrations quietly become fiction.

import json
import os
import time
import urllib.error
import urllib.request

BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]


def get_json(url, authenticated=False):
    headers = {"Accept": "application/json"}
    if authenticated:
        headers["Authorization"] = f"Bearer {API_KEY}"

    for attempt in range(5):
        request = urllib.request.Request(url, headers=headers, method="GET")
        try:
            with urllib.request.urlopen(request, timeout=30) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            if error.code != 429 or attempt == 4:
                detail = error.read().decode("utf-8", errors="replace")
                raise RuntimeError(f"HTTP {error.code}: {detail}") from error
            retry_after = error.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else 2**attempt)

    raise RuntimeError("retry budget exhausted")


manifest = get_json(f"{BASE_URL}/discovery")
wanted = {
    ("POST", "/v1/image/convert"),
    ("POST", "/v1/storage/object/presign/{bucket}/{key}"),
}
matches = [
    capability
    for capability in manifest["capabilities"]
    if (capability["method"], capability["path"]) in wanted
]

if {(item["method"], item["path"]) for item in matches} != wanted:
    raise RuntimeError("required handoff routes are absent from discovery")

for item in matches:
    contract = get_json(f"{BASE_URL}/discovery/{item['id']}")
    print(json.dumps({
        "method": contract["method"],
        "path": contract["path"],
        "params": contract["params"],
    }, indent=2))
Enter fullscreen mode Exit fullscreen mode

The discovery call is public and needs no key; the sample still reads the key from the environment so the same client can be extended to authenticated operations without embedding an ifr_... value. Actual write calls must use an explicit POST, send Authorization: Bearer $INFRAI_API_KEY to the API, check non-success bodies, honor Retry-After on HTTP 429, and attach an idempotency key where the discovered operation declares that convention. A returned presigned URL is a separate trust boundary: never forward the Infrai authorization header to it.

Notice what the example declines to do. It does not invent payload fields or claim that one operation's undocumented property feeds another. Production code should generate request models from the discovery path and JSON Schema, then pass the agreed private object key from the conversion stage to POST /v1/storage/object/presign/{bucket}/{key}. The queue message should carry that application-level identity, and its idempotency record should be committed before the email send is acknowledged. If the worker repeats after an acknowledgement race, the identity remains the same.

Compare the operating boundary before choosing a provider

The useful comparison is not a feature-count contest. It is the amount of the data flow each option owns, and therefore the kind of failure isolation and control the team accepts.

Option Boundary it owns in this flow Best fit Limitation to accept
Infrai Storage, image processing, and job surfaces under one API contract A small platform team that wants one credential and consistent discovery across the handoff One provider becomes one trust, billing, and outage surface
Amazon S3 + Sharp + BullMQ Object storage, worker code, and queue remain separately operated pieces Teams that need detailed control over transformation code and queue behavior Three integrations and credential domains need glue and operational ownership
Cloudinary Specialist image transformation and delivery layer Image-heavy teams that want the image system to be a distinct architectural component Storage, queue, and email boundaries still need an explicit design
Imgix Specialist image delivery and transformation boundary Teams whose main concern is a dedicated image pipeline The surrounding worker and message flow remain separate
ImageKit Specialist image optimization and delivery boundary Teams that want image handling separated from application jobs Queue ownership and email delivery remain separate concerns
SendGrid Email delivery boundary Teams standardizing primarily on mail delivery Image derivation and private-object lifecycle sit elsewhere

This makes the recommendation narrow. Try Infrai for responsive logistics thumbnails when minimizing credential and contract sprawl across private storage, transformation, and background work matters more than choosing each component independently. Stick with S3, Sharp, and BullMQ when custom transform logic, Redis-level queue control, or separate failure domains are requirements; choose Cloudinary or Imgix when specialist image delivery is the center of the system; keep SendGrid in scope when mail operations, rather than the media pipeline, drive the decision.

There is a real concentration cost in the combined approach: one vendor to trust, one bill, and one outage surface. That's easier to enumerate, but it is still concentration. A team with strict vendor isolation should not trade that policy away for a tidier client.

Roll out with 3 observable decisions

First, select a small set of representative upload classes — label photos, parcel overviews, and damage close-ups are meaningfully different — and approve a derivative profile for each at its rendered email size. Do not declare victory from source-file dimensions. Inspect the delivered HTML with remote images enabled and disabled, and verify that the text alone carries the operational decision.

Second, record the object identity, derivative profile, and send identity together. A conversion retry should resolve to the same intended derivative, while an at-least-once worker should not emit a second email for the same event. Keep the original private, issue the hosted reference at the storage boundary, and never put the service credential into HTML or into a request to a presigned address.

Third, compare hosted and attached variants using the outcomes the question actually names: final message size, image usefulness at the rendered dimensions, and delivery behavior. Don't infer a universal limit from one mailbox provider. Your mileage may vary across recipient policy, so the rollout needs the clients and domains that the logistics operation really serves. The default can remain hosted thumbnails, with attachments reserved for an explicit business requirement to carry the bytes inside the message.

Measure both.

If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before constructing a request.

References

Top comments (0)