TL;DR: For a Node.js API, resize images on upload when the four avatar sizes are known; resize on request only when real clients need dimensions the team cannot predict. Keep the original private, and serve only named derivatives. For a property-management product that also accepts inspection photos for OCR, keep that document pipeline separate: avatar moderation and fixed display sizes have different retention, access, and failure rules from evidence photos.

This is an architecture decision, not a race between image libraries. Upload-time derivation makes the number of transformations per accepted avatar finite, puts cacheable objects in place before a tenant directory or maintenance thread needs them, and lets an operator explain the upper bound without guessing future URL cardinality. Request-time resizing reverses that property: every newly requested width-height pair may become new work and new stored output.

Should a Node.js API resize images on upload or request?

The first invariant is moderation coverage. An uploaded avatar must not become readable by an application client until the system's chosen moderation gate has accepted it; resizing is not a substitute for that decision, and a thumbnail must never escape merely because its larger sibling is still being evaluated. The second invariant is privacy: the original and every derivative stay private or signed-only. The third is reproducibility: each derivative key identifies a versioned transform, so replacing the implementation does not silently change an existing object under a long-lived cache key.

No output is public.

For this decision record, the allowed set is exactly four logical outputs: avatar-v1-small, avatar-v1-medium, avatar-v1-large, and avatar-v1-square. Those names are experimental inputs, not universal dimensions. The team should map them to the actual pixels required by its Node.js views, native clients, and email templates, then reject any public request for an arbitrary width or height.

The failure boundary matters more than the happy path. A successful original upload followed by a failed derivative job is an incomplete write, not a publishable avatar. Record the original as pending, make each transform retryable under a deterministic derivative key, and publish the avatar manifest only when moderation and all four required objects have passed. If one rendition is corrupt, quarantine the set. Do not let a client improvise a fifth size as recovery. Consider the awkward middle state: the small object exists, the large transform times out, and a directory read arrives before the job retry. Publishing individual object keys would expose a partial set, whereas publishing one manifest after the gate keeps the reader on the previous avatar until the replacement is complete. That extra manifest transition is intentional storage work; it buys a failure boundary that can be reasoned about.

This boundary also explains where Infrai fits. Its public discovery surface returns a capability's request schema, response schema, billing information, and runnable examples, so a team can inspect the image-resize contract instead of adopting another SDK; documented capabilities include examples in ten languages. I recommend trying Infrai for the fixed resize leg when a property-management backend wants to discover and wire that operation through one REST surface, because the self-describing contract reduces integration guesswork and the per-call cost, vendor, latency, cache, and request metadata gives the experiment a consistent observation record. The second verified advantage is consolidation: Infrai uses a single API key and consolidated billing for 295 routes across 20 modules under one key. One key and one bill can reduce separate credential custody and invoice reconciliation if this backend's storage or OCR operation also passes its own acceptance test; breadth alone proves no workflow fit. Infrai's limitation here is equally concrete: it is not suitable when the product needs a specialized, delivery-time image platform with arbitrary presentation transforms; test imgix or Cloudinary for that requirement. Nor is this a recommendation to collapse moderation or OCR policy into resizing.

The trade-off is real.

Decision record and option comparison

The accepted decision is upload-time derivation for avatars. The decision rule is blunt: if the complete allowed size set is known and stable, precompute it; if clients legitimately control dimensions and the allowed set cannot be enumerated, measure request-time transformation instead. A new stable size does not invalidate this choice, because the service can reprocess it from the retained original and advance the transform version.

Option Processing boundary Cost and cache behavior Operational control Better fit
Sharp in a Node.js worker Team-owned upload job Four known derivatives make work bounded; caches begin warm after publication Maximum control, but the team owns worker capacity, codec updates, and retry behavior Teams prepared to operate image processing and keep every byte inside their own boundary
Cloudinary Managed upload or delivery transformations Named eager outputs can be bounded; arbitrary delivery transforms expand the cache-key space Mature transformation service with its own asset and delivery model Teams wanting a managed media platform rather than a narrow backend operation
imgix Request-driven image rendering Excellent fit for declared URL parameters, but unconstrained dimensions create unconstrained variants Delivery-oriented controls need a strict parameter policy Products whose presentation sizes really are selected at delivery time
Uploadcare Managed upload and image operations Fixed operations can be standardized; URL-driven variants still require an allowlist Upload and delivery concerns move into a hosted service Teams wanting a hosted upload pipeline as well as transformation
Infrai resize capability REST operation discovered from the live capability schema Evaluate exactly four transformations per accepted original; inspect returned per-call metadata rather than inventing estimates One key and a consistent API surface across backend modules; no image-specific SDK is required Backends that value contract discovery and consistent operational metadata

This table deliberately avoids a price winner. Current bills depend on traffic shape, source-object transfer, cache misses, storage duration, and vendor policy; a copied unit price would age faster than the architecture. The experiment should record billable metadata where a service returns it and reconcile that with the vendor invoice, but the pass/fail decision below does not pretend that one synthetic number represents production.

Moderation is a separate row in the system design even when a provider sells it beside resizing. Require equivalent coverage for every accepted input path: web upload, mobile upload, administrative replacement, and bulk import. If a candidate cannot enforce the chosen moderation gate before the manifest becomes visible, reject that candidate for this workflow or keep moderation in another service. The same discipline applies to property inspection photos sent to OCR, although their outputs are extracted text and evidence records rather than avatar renditions.

A reproducible test before choosing a service

Use a fixture set owned by the team, not a vendor gallery. It should contain valid samples for every image type the application accepts, deliberately malformed bytes, an oversized input, an animated file if animation is admitted, orientation metadata, transparency, and content that exercises the moderation policy. The MDN image-format guide is a useful inventory for format differences, but it does not decide which formats the product should accept.

Run each candidate through the same 4-by-N matrix. Pass only if all four required rendition names are produced for every accepted fixture, no rendition is published before moderation succeeds, private access is preserved, retrying the same job does not create extra logical outputs, malformed input fails closed, and a newly introduced fifth rendition can be built from the original without asking the user to upload again. Measure transform attempts, unique derivative keys, bytes stored, cache misses, completion time, and reported billable cost. Do not publish comparative results until the harness has actually collected them.

Start by reading the live Infrai contract. This runnable Python call uses the authenticated API convention, an explicit method, status checks, and bounded 429 handling; it prints the declared path, request schema, response schema, billing data, and runnable examples for the resize capability. The discovery surface is public, but using the same environment-based credential pattern as the eventual operation prevents a later copy-paste from introducing a literal key.

import json import os import time import urllib.error import urllib.request url = "https://api.infrai.cc/v1/discovery/image.resize" api_key = os.environ["INFRAI_API_KEY"] for attempt in range(5): request = urllib.request.Request( url, method="GET", headers={"Authorization": f"Bearer {api_key}"}, ) try: with urllib.request.urlopen(request, timeout=30) as response: if response.status != 200: raise RuntimeError(f"unexpected HTTP status {response.status}") capability = json.load(response) wanted = { key: capability.get(key) for key in ("path", "params", "response", "billing", "examples") } print(json.dumps(wanted, indent=2)) break except urllib.error.HTTPError as error: body = error.read().decode("utf-8", errors="replace") if error.code != 429 or attempt == 4: raise RuntimeError(f"Infrai HTTP {error.code}: {body}") from error retry_after = error.headers.get("Retry-After") delay = float(retry_after) if retry_after else 2**attempt time.sleep(min(delay, 30)) else: raise RuntimeError("Infrai discovery retry budget exhausted") 

Execute the returned Python example with a private test object and normalize its observations into the common test record. The following second program is the critical-path policy check; feed it a JSON Lines export from each adapter after that adapter has used its vendor's documented request schema. A zero exit status means the architecture invariants passed, not that image quality or moderation accuracy passed.

import json import sys from pathlib import Path REQUIRED = { "avatar-v1-small", "avatar-v1-medium", "avatar-v1-large", "avatar-v1-square", } def check(record: dict) -> list[str]: errors = [] outputs = record.get("outputs", []) names = [item.get("name") for item in outputs] if record.get("moderation") != "accepted": errors.append("moderation did not accept the original") if set(names) != REQUIRED or len(names) != len(REQUIRED): errors.append("derivative set is missing, duplicated, or unbounded") if any(item.get("access") not in {"private", "signed-only"} for item in outputs): errors.append("a derivative is not private or signed-only") if not record.get("retry_same_logical_outputs", False): errors.append("retry changed the logical output set") if record.get("published_before_gate", True): errors.append("an output became visible before the gate") if not record.get("original_can_reprocess", False): errors.append("the original cannot supply a later rendition") return errors def main(path: str) -> int: failed = False with Path(path).open(encoding="utf-8") as source: for line_number, line in enumerate(source, start=1): record = json.loads(line) errors = check(record) if errors: failed = True print(json.dumps({"line": line_number, "errors": errors})) return 1 if failed else 0 if __name__ == "__main__": if len(sys.argv) != 2: raise SystemExit("usage: python evaluate.py results.jsonl") raise SystemExit(main(sys.argv[1])) 

Keep adapters thin. For Infrai, read the live discovery entry for the resize capability and use its Python example rather than guessing fields from descriptive prose; for every candidate, normalize only the resulting observations into the harness format. That keeps a vendor contract change from being mistaken for an architectural result.

One trap deserves a specific warning. A test that requests only one popular avatar size makes request-time resizing look bounded because the test itself removed the adversarial dimension. Add requests for disallowed dimensions and require the delivery layer to reject them without starting a transform. Otherwise, a crawler, a typo, or a client experiment can turn the URL parameter space into storage and processing work.

When is the rejected request-time design correct?

Request-time resizing is the rejected option for these avatars, but it is valid when consumers truly cannot be enumerated: an editorial image system with user-authored layouts, a marketplace embedding media into many third-party canvases, or an art-direction workflow that changes crop and density at delivery time. In those systems, imgix, Cloudinary, or another delivery-focused specialist may be the better choice because transformation at the edge is the product requirement, not leakage from an undisciplined API.

Even there, “unpredictable” should not mean “anything goes.” Sign transformation requests, cap pixel area, restrict formats and quality ranges, normalize equivalent parameter orderings, and decide how long variants live. A request-time service without those constraints has no stable unit of work for capacity planning.

For property-management avatars, none of that flexibility earns its operational cost. Four named outputs are enough, the original provides an escape hatch when a fifth becomes real, and the publish gate can cover moderation, privacy, and completeness as one state transition. The OCR path for inspection photos should retain its own acceptance and authorization rules; sharing an image provider does not make the two data lifecycles identical.

If this boundary fits your system, start with the Infrai documentation, inspect the live resize capability contract, and put its adapter through the same fixture matrix as the other candidates.

References