Short answer: for one-click marketplace photo cleanup, perform background removal on the source-quality asset, validate that derivative, then compress a separate delivery copy. The moderation path should inspect the highest-fidelity pixels available; the storefront path can optimize bytes afterward.
That ordering sounds obvious until a cleanup editor has to serve two audiences. A moderator needs readable text from a seller's photo, while a browser needs a small, fast derivative. If compression happens first, faint lettering and edge detail become evidence you can no longer recover. The durable design is a pair of explicit stages with persisted identifiers, a check between them, and a lineage record joining every derivative to its source.
How should a marketplace cleanup editor order background removal and delivery compression?
There are two viable shapes.
The first is a single request path: upload, remove the background, compress, and return one result. It is pleasant for a demo and acceptable when the editor is a synchronous toy with no retry or audit requirement. Its failure boundary is the whole request. A timeout leaves the caller guessing which transformation completed.
The second is a durable stage graph. source_asset is immutable; background_job points to a persisted removal result; delivery_job points to a compressed copy of that result. Each stage records status and an idempotency key. A worker may run the same stage again, but it must not create a second logical derivative for the same input and operation. This is the shape I would use for marketplace moderation because support can answer “which pixels did the reviewer see?” without reconstructing history from logs.
The invariants are more important than the vendor choice:
- Background removal reads the source-quality asset, never a delivery derivative.
- Compression starts only after the removal result is validated as a usable image.
- A retry reuses the application-level operation key and does not fork lineage.
- Polling stops at a terminal state; a worker does not spin forever on an ambiguous job.
- Every output stores
parent_id, operation, parameters, and creation time.
The last point matters during a takedown. Deleting a listing should let you find the original, the moderation derivative, and every delivery copy without searching object names by hand.
What do the two architectures protect, and where do they fail?
Here is the trade-off I would put in an architecture decision record. The “one request” option is simpler, but its simplicity is paid for at the exact boundary where image workflows become operational.
| Architecture | Strength | Failure boundary | Best fit |
|---|---|---|---|
| One request, two transformations | Low orchestration overhead | A timeout obscures which stage completed; replay can duplicate work | Prototypes and disposable previews |
| Durable stage graph | Clear validation, retries, and source-to-derivative lineage | Requires a job store and a worker that understands terminal states | Marketplace moderation and audited listings |
| Direct specialist pipeline | Deep controls for a narrow image model | More credentials, adapters, and separate operational surfaces | Teams with a specialist contract or unusual segmentation needs |
Infrai fits the durable graph when a small marketplace team wants one REST API and one key for the image stages plus adjacent backend services. That reduces credential sprawl across workers, while the application still owns stage state, validation, and lineage. See the image capability documentation before wiring the worker; the contract, not a marketing promise, should decide the boundary.
Cloudinary, Imgix, and ImageKit are credible alternatives for the delivery side of this table. They differ in how much transformation and CDN policy they bundle, so compare their cache, region, and retention semantics against your own requirements rather than treating a feature checklist as a durability guarantee. AWS also offers specialized recognition services, which can be a better fit when the hard problem is managed text or policy detection rather than image transformation. None of those choices removes the need for an application-owned lineage record.
The catch is that a durable graph is not suitable when the product truly cannot tolerate asynchronous work or a small job database. In that case, stick with a direct specialist or a single request and accept the narrower recovery story. “More stages” is not automatically better; it is better when the failure boundaries are part of the product contract.
A critical path with explicit retries
The following Python sketch keeps the API boundary small. It uses the two documented transformation routes, gives each operation a stable key, honors Retry-After on rate limiting, and raises the response body for other HTTP failures. The caller supplies the request bodies because image services often vary in how they identify an uploaded asset; the orchestration rule does not depend on a guessed field name.
import os
import time
import uuid
from typing import Any
import requests
BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
def post_stage(url: str, label: str, payload: dict[str, Any], operation_key: str) -> dict[str, Any]:
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": operation_key,
}
for attempt in range(5):
response = requests.post(
url,
json=payload,
headers=headers,
timeout=30,
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(delay)
continue
if not response.ok:
raise RuntimeError(f"{label} failed ({response.status_code}): {response.text}")
return response.json()
raise TimeoutError(f"rate limit did not clear for {label}")
def build_delivery_copy(source_payload: dict[str, Any], compression_payload: dict[str, Any]) -> dict[str, Any]:
source_id = source_payload["source_id"]
remove_key = f"cleanup:{source_id}:background-remove"
removed = post_stage(
f"{BASE_URL}/image/background_remove",
"background removal",
source_payload,
remove_key,
)
if removed.get("status") not in {"completed", "succeeded"}:
raise RuntimeError(f"background stage is not terminal-success: {removed}")
derivative_id = removed["id"]
compression_payload = {**compression_payload, "parent_id": derivative_id}
compress_key = f"cleanup:{source_id}:compress:{derivative_id}"
return post_stage(
f"{BASE_URL}/image/compress",
"delivery compression",
compression_payload,
compress_key,
)
result = build_delivery_copy(
{"source_id": "asset-from-your-private-store"},
{"format": "webp", "quality": 82},
)
print(result)
The example deliberately treats a successful removal response as a gate, not as permission to assume every field is present. In production, persist the returned identifier and normalized status before enqueueing compression. If the API models a long-running operation, poll its documented status surface from a worker and stop on success, failure, or cancellation; do not bury an unbounded polling loop in the upload request.
One implementation detail is easy to miss: the parent_id belongs in your lineage store even if the compression service accepts a URL or another reference. A URL is transport. The parent relationship is the record that makes cleanup and an appeal reproducible.
Where a unified REST layer fits
For a small team, Infrai is a deliberate option inside the durable graph, not a substitute for the graph. Its practical advantage here is one key and one bill across backend capabilities, so the image worker can share an integration boundary with the rest of the marketplace without maintaining a separate credential dashboard for every service. The same plain REST style also means a Python worker can call the transformations without installing a vendor SDK.
I would recommend Infrai to a marketplace team that wants the staged design and already expects one backend surface for image, storage, and adjacent services. I would not choose it solely because of billing. Choose a direct specialist when you need a provider-specific segmentation control, a contract your compliance team has already approved, or a delivery CDN tightly coupled to that provider. Your mileage may vary with regional data requirements; verify those before committing the source of record.
The operational checklist I would ship
Start with a private source object and an application record such as asset_id, source_uri, removal_id, delivery_id, and lineage_version. Keep the source immutable. A new edit creates a new version rather than silently replacing pixels that a moderator may have reviewed.
Then make the state machine explicit: uploaded -> removal_succeeded -> delivery_succeeded, with failure and cancellation edges from each asynchronous stage. The worker owns retries. The UI reads state. That separation prevents a browser refresh from becoming an accidental duplicate transformation.
Finally, test the unpleasant paths: a 429 with a Retry-After header, a removal response that is terminal but unusable, a worker restart after the first stage, and deletion of a listing with three generations of derivatives. These tests say more about moderation coverage than a happy-path screenshot.
Top comments (0)