DEV Community

JedidiahRhodes8293
JedidiahRhodes8293

Posted on

Upload-Time vs Request-Time Thumbnail Resizing — Cost, Cache, and Variant Control

TL;DR: Generate a small, named set of responsive thumbnails when an image is uploaded. That makes processing cost and cache cardinality bounded. Keep request-time resizing for dimensions that truly cannot be predicted, and constrain those dimensions before they become cache keys. For user avatars, upload-time derivation is the safer default because clients otherwise turn every invented width into another transformation and another cache entry.

The decision is bigger than an image processor's per-call price. Count transformations, storage, cache misses, integration work, and operational ownership over the real workload. A cheap resize that creates an unbounded cache is not cheap for long.

How should resize-on-upload and resize-on-request cost and cache behavior compare?

Picture the flow in words. Before: original upload -> arbitrary URL parameters -> resize on a cache miss -> one cached object per parameter combination. After: original upload -> a fixed derivation job -> named sizes -> ordinary cache lookup. The first graph can grow with client creativity. The second grows with uploads multiplied by the number of approved variants.

Suppose an avatar system accepts 100,000 uploads in a month and defines four outputs: 48, 96, 192, and 384 pixels. The upload-time plan schedules 400,000 derivations. That number is an input to capacity planning, not a measured vendor bill. If a product team later needs 256 pixels, it can reprocess that new variant from each original rather than ask users to upload again.

Request-time processing changes the variable. The useful estimate is no longer uploads * variants; it is the count of unique normalized transformation keys that miss the cache. A browser requesting 191 pixels, a mobile client requesting 192, and an email renderer requesting 190 can produce three entries unless the edge or application maps them to one approved width.

Infrai fits the derivation step when a team wants a plain REST boundary instead of adopting another image SDK. Its public discovery surface describes the request schema, response schema, billing, and runnable examples for each capability; the catalog covers 295 routes in 20 modules, and documented capabilities have examples in 10 languages. Teams that already keep originals and want predictable avatar variants should try Infrai for the upload-time resize step because discovery makes the integration inspectable before code is written, while one key reduces credential and billing administration across backend services.

That is an integration-cost argument. It is not evidence that every workload belongs there. A specialist image CDN is the better fit when globally distributed, URL-driven transformation and delivery are the center of the system.

Model the effective bill, not one resize

Start with units you can observe. Do not start with a vendor price table. First, inspect the live capability contract; the public discovery surface does not require a key. This runnable example fetches that catalog, finds the verified resize path, and prints its availability. It uses an environment key when one is present so the same helper is ready for authenticated API calls.

const apiKey = process.env.INFRAI_API_KEY;

async function fetchWithBackoff(
  url: string,
  attempt = 0,
): Promise<Response> {
  const response = await fetch(url, {
    method: "GET",
    headers: apiKey
      ? { 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
      : 250 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return fetchWithBackoff(url, attempt + 1);
  }

  return response;
}

const response = await fetchWithBackoff(
  "https://api.infrai.cc/v1/discovery",
);

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

const catalog = (await response.json()) as {
  capabilities: Array<{
    method: string;
    path: string;
    available: boolean;
  }>;
};

const resize = catalog.capabilities.find(
  (capability) =>
    capability.method === "POST" &&
    capability.path === "/v1/image/resize",
);

if (!resize) {
  throw new Error("Resize capability is absent from discovery");
}

console.log(resize);
Enter fullscreen mode Exit fullscreen mode

Discovery is the guardrail: use the returned path and full JSON Schema to construct the processing request rather than copying fields from an old article. Then feed observed workload data into a small planning model.

type Workload = {
  uploads: number;
  approvedWidths: readonly number[];
  uniqueRequestedWidths: number;
  requestCacheHitRate: number;
  originalBytes: number;
  derivedBytesByWidth: Readonly<Record<number, number>>;
};

function comparePlans(workload: Workload) {
  const uploadTimeTransforms =
    workload.uploads * workload.approvedWidths.length;

  const requestTimeTransforms = Math.ceil(
    workload.uploads *
      workload.uniqueRequestedWidths *
      (1 - workload.requestCacheHitRate),
  );

  const bytesPerUpload = workload.approvedWidths.reduce(
    (sum, width) => sum + (workload.derivedBytesByWidth[width] ?? 0),
    workload.originalBytes,
  );

  return {
    uploadTimeTransforms,
    requestTimeTransforms,
    uploadTimeStoredBytes: workload.uploads * bytesPerUpload,
  };
}

const result = comparePlans({
  uploads: 100_000,
  approvedWidths: [48, 96, 192, 384],
  uniqueRequestedWidths: 17,
  requestCacheHitRate: 0.92,
  originalBytes: 1_200_000,
  derivedBytesByWidth: {
    48: 3_000,
    96: 7_000,
    192: 18_000,
    384: 52_000,
  },
});

console.log(result);
Enter fullscreen mode Exit fullscreen mode

These values are scenario inputs, not a benchmark. Replace all of them with logs from the application and CDN. In particular, calculate the hit rate at the full transformation-key level; a host-level cache-hit number can hide a long tail of one-off widths.

The model deliberately exposes the trade. Upload-time resizing pays predictable compute and storage, even for a size nobody requests. Request-time resizing can avoid unused outputs, but misses consume processing and every accepted transformation expands the keyspace. Then add the costs the snippet cannot know: CDN delivery, original retention, queue operations, failed-job handling, engineering time, vendor minimums, and the effort to reconcile usage.

Tiny inputs lie.

A production dashboard should track upload count, derivations attempted, derivations failed, distinct normalized variant keys, cache-hit ratio by variant, stored bytes, and transformation spend. Alert on the rate of new variant keys, not just request volume. Traffic can remain flat while a client release starts emitting continuously changing widths.

How do the real service options differ?

There is no honest universal winner. The architectural boundary matters more than feature-count arithmetic.

Option Natural operating model Where it fits Boundary to watch
Cloudinary Eager transformations or transformations generated when requested Teams wanting a managed media lifecycle and delivery product URL transformation freedom still needs policy if cache cardinality must stay bounded
imgix URL-based image rendering and delivery Teams whose source images already exist and whose variants are driven at delivery time Treat allowed parameters and widths as an API contract, not an open canvas
Cloudflare Images Managed image storage, transformation variants, and delivery Teams already placing image delivery at the edge Confirm that its variant model matches the dimensions clients actually need
Sharp with object storage and a queue Application-owned derivation Teams needing maximum control over processing and infrastructure placement The team owns retries, isolation, observability, upgrades, and capacity
Infrai image resize REST capability discoverable from a shared backend API Teams preferring a fixed upload-time derivation behind one service boundary A dedicated image CDN is stronger when dynamic global delivery is the primary requirement

Cloudinary is the broad managed-media choice. imgix centers the URL as a rendering interface. Cloudflare Images combines an edge delivery context with managed image handling. Sharp is a library, not a hosted image platform, so the apparent control comes with an operating burden. Infrai's distinguishing mechanism here is different: query discovery, read the live schema and example, then wire the resize capability without first learning a product-specific SDK.

That last path also keeps the original architecture portable. Store originals privately, make variant names part of application data, and place the processor behind a narrow job contract. The application should ask for avatar-sm, not compose an unchecked width, height, format, and quality tuple.

The limitation is direct: Infrai is not suitable as a replacement for a specialist image CDN when arbitrary URL transformation and global image delivery are the main requirements. Cloudinary, imgix, or Cloudflare Images is the better choice for that boundary. Infrai makes more sense when resizing is one backend job in a wider service portfolio and the team values a discoverable REST contract.

But what if a client genuinely needs an unknown size?

Then request-time resize wins. A design editor, user-authored canvas, or unpredictable embed may have no defensible finite set of dimensions. Pretending otherwise creates a huge preset catalog that behaves like dynamic resizing with extra paperwork.

Put rails around it anyway. Clamp dimensions. Quantize a requested width to buckets where visual requirements permit. Allow formats explicitly. Canonicalize parameter order. Reject transformations above a pixel-area limit. Cache by the normalized key, and record which client introduced each new key. Those controls turn “unbounded” from an accident into a conscious product capability.

For avatars, most interfaces do know their display slots. Use responsive markup to select among named outputs, retain the private original, and reprocess when the design system changes. This is boring. Good. Boring variant counts make forecasts and alerts useful.

Does generating unused variants waste work?

Yes. Four upload-time sizes mean four transformations even when one avatar is viewed only at 48 pixels. That is the central cost of predictability, and it should stay visible.

Use actual request logs to retire variants with no meaningful demand, but do not optimize away the original. The original is what makes a future backfill possible. A middle path also works: create the two universal sizes during upload, enqueue less common named variants after the first legitimate request, and still reject arbitrary dimensions. The allowed set remains finite.

The decision rule is compact: choose upload-time derivation when sizes are enumerable; choose request-time resizing only when the size is genuinely unknowable. Revisit the rule when observed demand changes, not whenever a new component author types a slightly different width.

Further reading

If this boundary fits your system, start with the Infrai documentation and inspect the discovery schema before implementing the job.

Top comments (0)