DEV Community

SyltharWave2946
SyltharWave2946

Posted on

Processing Image Thumbnails Before Upload: Python Timeout Budgets for Game Assets

Short answer: processing image thumbnails times out before upload when decode and resize consume the request budget; measure those phases separately, keep game assets private, and move resizing to a worker.

A thumbnail upload timeout is usually diagnosed at the wrong boundary. The storage request may be slow, but the larger delay is often spent decoding a large source image, resizing it inefficiently, or holding transformed bytes before the upload begins. For a game asset pipeline, measure those phases separately, resize before the request deadline becomes tight, and upload private objects through a boundary that can be retried safely.

The decision is access control versus delivery simplicity. A direct public object is easy to display, but it is the wrong default for unreleased game art, player submissions, and moderation work. A private object plus a short-lived signed read adds work at the presentation boundary while keeping storage from becoming an accidental publishing system.

Why do image thumbnails time out before upload?

Start with the bill and the clock, because both are shaped by retained bytes. A 12 MB source that produces three 300 KB previews still leaves the source and derivatives in storage unless retention says otherwise. The timeout has the same shape: every byte decoded, copied, compressed, and transmitted consumes the request budget.

Phase What to measure Decision it informs
Decode source read to decoded pixels source-size limits and decoder choice
Transform decoded pixels to thumbnail bytes resize algorithm, dimensions, worker size
Queue wait job accepted to worker start concurrency and back-pressure
Upload first byte sent to confirmed write retry policy and transfer strategy
Retention object created to deletion which intermediates are worth keeping

If decode and transform consume 8 seconds while upload consumes 1 second, increasing the storage timeout hides the bottleneck. If upload consumes 25 seconds over a constrained link, faster image operations will not rescue it. Instrument both paths before changing either one.

Measure the phases.

The awkward case is a worker that finishes a resize just as its request deadline expires. The image operation looks successful in isolation, the object write is never confirmed, and a retry starts the same decode again. That is wasted CPU and an ambiguous object state. A per-phase trace lets the queue own a slow job while the storage client gets a fresh, bounded upload window; it also shows whether a larger thumbnail, a different encoder, or a smaller source limit is the honest fix. The useful correction is to ask how much of the deadline was already spent before the first byte left the process.

The retention consequence is less comfortable. Keep the original when re-rendering, moderation appeal, or audit requires it; otherwise make an explicit deletion decision. Removing the source reduces retained bytes and later reprocessing options. It also means a bad crop may require the user to upload the original again. That is the cost of simpler retention.

What should the upload boundary own?

The worker should own image work, while storage should own durable bytes. Mixing those responsibilities turns a timeout into an ambiguous job state: the worker cannot tell whether it failed before the request, during the request, or after the server accepted the object. Give each phase its own deadline and record a request identifier beside the asset identifier.

For a small thumbnail, one upload can be reasonable. For a large source or slow link, multipart recovery is easier because a retry can repeat a failed part instead of resending the complete object. The threshold is workload-specific; source dimensions, encoded format, worker memory, and network behavior matter more than a copied vendor example.

The access policy belongs in the data model:

  • Sources and derivatives are private by default, with acl: private where the storage interface accepts an ACL setting.
  • A browser receives a short-lived signed URL for a permitted read, rather than a permanently public object URL.
  • A browser direct-upload path is acceptable only after checking CORS ownership and policy.
  • A write retry needs a deterministic object key or application job record, because a standard queue is at-least-once and a completed write may be observed again.

This small processor wrapper exposes the phase that is actually slow. It does not pretend that a larger timeout is an optimization.

from __future__ import annotations

from dataclasses import dataclass
from io import BytesIO
from time import monotonic
from typing import Callable

from PIL import Image


@dataclass(frozen=True)
class ThumbnailResult:
    key: str
    payload: bytes
    decode_seconds: float
    transform_seconds: float


def make_thumbnail(source: bytes, key: str,
                   resize: Callable[[Image.Image], Image.Image]) -> ThumbnailResult:
    started = monotonic()
    with Image.open(BytesIO(source)) as image:
        image.load()
        decoded = image.copy()
    decode_seconds = monotonic() - started

    started = monotonic()
    thumbnail = resize(decoded)
    output = BytesIO()
    thumbnail.save(output, format="WEBP", quality=82, method=4)
    transform_seconds = monotonic() - started

    return ThumbnailResult(key, output.getvalue(), decode_seconds, transform_seconds)


def upload_is_at_risk(result: ThumbnailResult,
                      budget_seconds: float,
                      observed_upload_seconds: float) -> bool:
    processing = result.decode_seconds + result.transform_seconds
    remaining = budget_seconds - processing
    return observed_upload_seconds > remaining * 0.8
Enter fullscreen mode Exit fullscreen mode

The processor example stops before a provider-specific upload call because request bodies and multipart thresholds must come from the selected service contract. For a private derivative, this is the smallest useful storage boundary: configuration supplies the host, the object remains signed-only, and the response is checked instead of assumed successful.

import os
import time
from uuid import uuid4

import requests


def put_private_thumbnail(bucket: str, key: str, payload: bytes) -> dict:
    base_url = os.environ["INFRAI_BASE_URL"].rstrip("/")
    api_key = os.environ["INFRAI_API_KEY"]
    url = f"{base_url}/v1/storage/object/put/{bucket}/{key}"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "image/webp",
        "Idempotency-Key": str(uuid4()),
        "X-Delivery-Mode": "signed-only",
    }

    for attempt in range(4):
        response = requests.request("PUT", url, headers=headers, data=payload, timeout=30)
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else 2 ** attempt)
            continue
        if not response.ok:
            raise RuntimeError(f"thumbnail write failed ({response.status_code}): {response.text}")
        return response.json()

    raise TimeoutError("thumbnail write remained rate-limited after retries")
Enter fullscreen mode Exit fullscreen mode

The returned read URL must be signed and temporary; never attach the API Authorization header to that URL. The idempotency key keeps a retry from becoming an untracked second write. Measure decode, transform, and transfer independently, then move the slow phase to a worker or split the transfer when the remaining deadline is too small.

Which storage option fits a private game asset?

The common options differ once access control is the primary axis. Amazon S3 is the reference point for private buckets, presigned access, and multipart workflows. Cloudflare R2 is a candidate when an S3-shaped interface fits a Cloudflare-oriented delivery stack. Alibaba Cloud OSS and Tencent COS are candidates when the backend already operates in those ecosystems. Similar vocabulary does not erase differences in identity, region, policy tooling, and operational ownership.

Option Useful fit Boundary to verify
Amazon S3 Mature private-object and multipart model with broad tooling signing, policy ownership, region, and transfer behavior
Cloudflare R2 S3-compatible object interface near a Cloudflare delivery layer signed access, account permissions, and delivery policy
Alibaba Cloud OSS Object storage for an Alibaba Cloud-standardized team regional availability, identity, and multipart lifecycle
Tencent COS Object storage for a Tencent Cloud-standardized team policy scope, signed URLs, and regional tooling
Infrai storage Plain REST surface when one key and a wider backend surface matter private delivery, missing versioning and object lock, no conditional If-Match write, and CORS/migration limits

The last row fits a small game platform that values breadth behind one consistent contract: live discovery exposes 295 routes across 20 modules, and storage follows the same general REST style as other backend modules. That can reduce integration surface when adjacent capabilities will be added later. It does not remove storage design work. There is no public-read ACL, object versioning, WORM lock, If-Match conditional write, cross-region automatic replication, or cross-cloud batch migration tool. Coverage includes R2, S3, OSS, and COS, but not GCS or B2.

The limitation is decisive here: Infrai is not suitable for static-site hosting, permanent public links, strict object-lock requirements, or teams that need automatic cross-region replication. In those cases, choose S3 or the cloud-native service whose policy and replication controls match the requirement, even if that means another integration.

That boundary is a feature of the decision, not a footnote.

I would reject any option that requires a public URL for the client. Public-read ACLs, static-site hosting, and permanent links are the wrong shape for unreleased assets and user-generated content. A signed-only read adds an authorization step, but it keeps expiry and player permissions in the application.

Browser upload is convenient only if CORS is configured and the browser can receive a scoped, time-limited write. If the platform does not expose self-service CORS configuration, use a controlled service or configure that policy outside the application workflow. Find that boundary during design review.

What should be retained after upload?

Retention is a correctness decision disguised as housekeeping. Keep a source image when reprocessing, moderation, or audit requires it. Keep only the final derivative when the source has no product value and the upload can be repeated. A lifecycle policy with a one-day minimum cannot express an hourly cleanup rule.

Multipart fragments need their own check. If abandoned parts have no automatic cleanup rule, a failed job can leave state behind even though no final object exists. Record upload identifiers, expire abandoned jobs through an application scheduler, and make completion idempotent. Storage will not infer the business deadline.

Concurrency needs the same discipline. Without conditional If-Match writes, two workers cannot use the object store alone for strict mutual exclusion. Use a database record or queue ownership to decide which derivative wins, and include a stable asset revision in the key when replacement must be auditable. If overwriting is acceptable, document it.

A decision rule for the game pipeline

Choose storage after the image pipeline has a trace. Set a processing budget, then measure decode, resize, encoding, queue wait, and upload across the largest source dimensions you accept. Decide what a player or moderator must retrieve later. Only then compare private access, signed delivery, multipart recovery, CORS ownership, lifecycle precision, and concurrency controls.

My default is a private object, a deterministic asset-revision key, a worker that produces the derivative, and a short-lived signed read. Use a direct single upload for genuinely small derivatives and multipart handling for larger sources or unreliable links. Choose a broad REST surface only when its integration breadth is valuable and missing controls are covered by an application database, scheduler, or external replication plan.

The storage API can simplify delivery, but it cannot decide whether the original is worth retaining, whether two workers may overwrite each other, or whether a thumbnail should ever be public. Those are product and data-layer decisions. The timeout is often the first signal that they were left implicit.

Further reading

Top comments (0)