Definition
This is the approach you use when you want a large user-uploaded file (image, video, etc.) to go straight from the client to storage without passing through the app server. The server only issues a special URL that says "upload to this address," and the actual file transfer happens directly between the browser and storage.
Let's unpack the terms first: object storage (S3, etc.) is cloud storage that stores each file as one whole object, and binary means the raw file data of a non-text file such as an image or video.
To be precise, a Presigned URL is a time-limited, permission-limited, one-time URL that the backend—the owner of the object storage—issues by signing it (attaching a cryptographic signature so it can't be forged) with its own credentials. A client that receives this URL can send a request directly to storage, without separate authentication (a login token, etc.), only within the scope embedded in the signature (which method, which path, until when). In other words, it is a delegation token in which the backend has decided in advance "who can do what, until when" and packed it into a single URL.
Why It's Needed
If you upload a large file through the application server, the server consumes memory, bandwidth, and request time relaying the entire upload traffic. In an environment with limited request body size or execution time (serverless-stateless-execution), this approach itself hits a wall. A presigned URL separates permission issuance (a lightweight metadata request) from the actual transfer (client↔storage directly). The server handles only small requests, and storage receives the heavy binary directly.
How It Works
The standard flow is organized into a four-step contract.
| Step | Call | Role | If missing |
|---|---|---|---|
| ① presign | POST /files → { fileId, uploadUrl, fileUrl } | Backend issues the signed upload address | Can't start the upload |
| ② PUT | PUT uploadUrl (body = binary) | Client → storage direct transfer | File not in storage |
| ③ complete | PATCH /files/{fileId} | Notify the backend "upload done" | "File not uploaded" error |
| ④ create | POST /resources (references fileId) | Create a resource from the uploaded file | Resource creation fails |
The key is the separation of ② and ③. Storage does not automatically notify the backend of a successful PUT. So for the backend to recognize that "this file is now usable," a separate ③ complete notification is required.
Practical Application
If you want to skip the ② PUT in a mock environment, decide based on whether uploadUrl is a dummy address (e.g., contains example.com) rather than a global mock flag. That way, a PUT is always executed against a real backend presign URL.
Tradeoffs
- Pros: reduced app-server load and bandwidth, avoiding serverless body-size limits, advantageous for large / numerous uploads.
- Cost: with more steps, client logic becomes more complex, and the client must exactly match the signature conditions such as expiry and content-type. Upload progress, retries, and partial-failure handling also become the client's responsibility.
When Not to Use It
- When the file is very small and it's simpler to send it in one go together with the metadata (little benefit from splitting steps).
- When the server must validate/transform content mid-upload, so the binary must necessarily pass through the server.
Common Mistakes
- Missing ③ complete: calling ④ without ② or without ③, hitting the "file not yet uploaded" error.
- Confusing
uploadUrlandfileUrl: you must PUT to the write address (uploadUrl), but you send to the read address (fileUrl). - content-type mismatch: the PUT header differs from the condition included in the signature, so signature verification fails.
- Ignoring expiry: PUT too late after issuance and the URL has expired.
Related Concepts
- serverless-stateless-execution — why direct upload that bypasses the server suits a stateless, short-lived function environment.