Short answer: for a large AI-generated image, let a trusted Node.js control plane create and authorize an upload session, let the worker send numbered bytes through short-lived presigned URLs, and finalize only an ordered manifest of returned ETags. Abort every session that will not finish.
This is a useful split because image generation and byte transfer fail differently. A generation retry can consume more model or accelerator work; a transfer retry should usually resend one part. Keeping those paths separate also keeps long-lived storage authority out of a browser or image worker.
How should a Node.js service presign multipart parts for AI-generated image uploads?
Start with two actors. The trusted API chooses the bucket and object key, binds them to a tenant and image job, starts the multipart session, and issues a presigned URL for one numbered part at a time. The worker holds the bytes, uploads each part, retains the response ETag, and asks the API to finalize or cancel. It does not choose a new key halfway through the transfer.
AWS describes a presigned URL as a way to grant time-limited access without giving the recipient storage credentials. Treat it as a bearer capability: keep its lifetime proportional to the transfer, do not put it in logs, and scope the application endpoint that asks for it. The effective lifetime can also be limited by the credentials that signed it, so an application deadline and URL expiry should be observed separately.
The application record needs more than a provider upload ID. Store the tenant, image-job ID, selected object key, session status, expiry, and an acknowledgement for each part number. Make the acknowledgement unique by session and part number. A duplicate acknowledgement with the same ETag is harmless; a different ETag for the same part should stop the finalization path until the conflict is understood.
Keep the authorization boring.
A small state machine before an SDK adapter
The storage adapter should be thin: it creates a session, produces a presigned part URL, finalizes a manifest, and aborts a session. The application owns the transitions. This Python model is runnable without selecting a storage vendor, and it makes the tests readable before an HTTP handler is involved.
from dataclasses import dataclass, field from enum import Enum class Status(str, Enum): OPEN = "open" COMPLETED = "completed" ABORTED = "aborted" @dataclass class UploadSession: session_id: str object_key: str status: Status = Status.OPEN etags: dict[int, str] = field(default_factory=dict) def acknowledge_part(self, part_number: int, etag: str) -> None: if self.status is not Status.OPEN: raise ValueError("upload session is closed") if part_number < 1 or not etag: raise ValueError("invalid part acknowledgement") previous = self.etags.get(part_number) if previous is not None and previous != etag: raise ValueError("part has conflicting ETags") self.etags[part_number] = etag def manifest(self) -> list[dict[str, int | str]]: numbers = sorted(self.etags) if self.status is not Status.OPEN or not numbers: raise ValueError("upload session cannot be completed") if numbers != list(range(1, len(numbers) + 1)): raise ValueError("part numbers must be contiguous") return [ {"PartNumber": number, "ETag": self.etags[number]} for number in numbers ] def complete(self) -> list[dict[str, int | str]]: result = self.manifest() self.status = Status.COMPLETED return result def abort(self) -> None: if self.status is Status.COMPLETED: raise ValueError("completed uploads cannot be aborted") self.status = Status.ABORTED I would put deterministic evals around this before connecting it to an endpoint: same-ETag duplicate acknowledgement, changed-ETag conflict, a missing part, finalization replay, cancellation, and two tenants probing one opaque session ID. Those checks are cheap, and they prevent a transfer bug from masquerading as a failed image-generation eval.
On the Node.js side, the handler maps its request into the same four commands: create, issue a URL for a known part, record the ETag, then complete or abort. The handler must validate ownership on every command. Its database transaction is the source of application intent; the object store is evidence of transferred bytes. Neither replaces the other.
Completion is a workflow boundary, not a successful request
An individual part response says something about that one upload request. It does not make a complete image available to a downstream RAG index, agent tool, or gallery. Finalization should submit the exact ordered part-number/ETag manifest recorded for the session. Only after finalization and a read-side verification appropriate to the chosen storage interface should the image job move into an available state.
That ordering is non-negotiable.
Consider the recovery path, because this is where a seemingly small shortcut becomes expensive. A worker can finish sending parts and lose the response from its request to finalize; meanwhile, a retry may arrive from the same job after a process restart. The trusted API should look up the stored session and manifest, enforce that the caller still owns the image job, and decide from recorded state whether it can safely repeat the finalize request or report that the image is already available. It should never rebuild a manifest from client guesses, accept a different object key, or turn a missing acknowledgement into an empty success. If the storage-side finalization is accepted, persist the terminal application state before publishing the image to the next pipeline stage. A verifier can then read the expected metadata through the storage interface and compare the object key, content type, byte length, and a generation-side checksum when that interface preserves comparable information. This gives operators a useful answer to a practical question: did generation produce bytes, did transfer assemble them, or did the publishing step withhold them? Those are distinct states, and collapsing them into one done flag turns an ordinary retry into a long debugging session. It also makes a transfer evaluation honest: the test can inject loss between requests without pretending that a successful HTTP status made an object readable.
That distinction matters for notebook-to-prod work. An image generator can emit the same bytes once, record a checksum and byte count, and hand transfer responsibility to a separate component. If a part needs another attempt, the system does not automatically request another generation. Token-cost accounting stays attached to generation, while retry count and transferred bytes remain transfer metrics.
Use three clocks: the URL lifetime, the application session deadline, and the cleanup schedule. Emit events for session creation, part acknowledgement, finalization requested, finalization recorded, verification recorded, and abort recorded. Include an opaque session ID, a hashed object key, elapsed time, part count, and byte count. Exclude the presigned URL itself.
I'm not sure a copied part size can be correct across different generated-image sizes and network conditions. Measure whole-object retry cost, memory pressure, request count, and tail latency with representative bytes. Small parts lower the amount resent after a failed part but create more acknowledgements; large parts reverse that trade-off.
When is direct multipart transfer the wrong choice?
The catch is that direct presigned multipart transfer is not suitable when a policy requires each byte to traverse an inspection service, when the client cannot retain the ETags needed for completion, or when a network intermediary prevents the worker from reading required response headers. Route bytes through a controlled service in those cases, accepting the bandwidth and buffering cost. A single-object upload is also a better fit when the generated images are small enough that repeating the full transfer is predictable and cheap.
| Boundary | Worker or client | Trusted API |
|---|---|---|
| Object identity | Holds an opaque session ID | Selects and binds bucket plus object key |
| Authorization | Presents application identity | Checks tenant and image-job ownership |
| Part evidence | Reads and submits the returned ETag | Persists and validates the manifest |
| Terminal action | Requests complete or cancel | Finalizes, verifies, or aborts |
Protocol compatibility is not a compliance claim. FedRAMP is a U.S. government program for standardized security assessment, authorization, and continuous monitoring; an S3-compatible interface alone does not establish a workload's authorization posture. Teams with an authorization requirement need to assess the deployed data flow, identities, locations, and operating controls with the responsible security organization.
There is a second operational boundary: abandoned sessions. A scheduled worker should select expired application sessions still marked open, request an abort through the adapter, and record the terminal transition. Keep it bounded and observable. Storage-layer lifecycle cleanup can provide defense in depth, but it cannot reconcile a job record or tell a user why their image is unavailable.
A release check for generated-image storage
Before release, run a fixed-byte transfer eval that covers a one-part success, several-part success, retry of one part, duplicate acknowledgement, missing acknowledgement, expired application deadline, cancellation, and finalization after the client lost its response. The acceptance condition is state you can inspect: an object becomes available only after verification, a cancelled session cannot later finalize, and each open session eventually reaches a terminal state or becomes eligible for cleanup.
Track generation latency, transfer latency, finalization latency, verification latency, retry count, bytes transferred, and age of open sessions separately. One blended job-duration chart hides the decision boundary that matters. Don't let an upload retry spend compute on a second image.

