---
title: "Upload straight to storage with a signed · Agent Wranglers"
url: https://agentwranglers.se/playbook/files-and-media/uploads/presigned-uploads
id: MED-41
level: Recommended
updated: 2026-10-22
build: a2a6bdfed830
tokens: 1289
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Upload straight to storage with a signed address

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Uploads](https://agentwranglers.se/playbook/files-and-media/uploads.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-22.

- ID: MED-41
- Level: Recommended
- Chapter: [Uploads](https://agentwranglers.se/playbook/files-and-media/uploads.md)
- Rule: Let clients send file bytes straight to object storage through a pre-signed address that your API issues, limited in size, type and time.
- Check: Ask for an upload address for a 10 MB limit. Upload a file 1 byte over the limit and storage refuses it. Wait past the expiry and upload again, and storage answers 403. The key in the address never contains the client's file name.
- How: The client asks your API for an upload. The API checks who is asking, picks the storage key itself and returns a signed address valid for 15 minutes at most. Sign a POST policy rather than a plain PUT, so storage itself refuses files over the size limit. Fix the content type in the policy too. Record the upload as `pending` in your database before you return the address. Your servers never carry the bytes, so a 2 GB video costs them nothing. Amazon S3 and Google Cloud Storage both sign POST policies. A plain PUT address can at most fix one exact size, by signing the `Content-Length` header where the store supports it. Azure Blob Storage SAS tokens cannot sign it at all. Where only PUT is offered, the [server check](https://agentwranglers.se/playbook/files-and-media/uploads/confirm-uploads.md) must refuse files over the limit. Check each file once it lands, as [Check every uploaded file](https://agentwranglers.se/playbook/application-security/requests-files-and-caches/safe-file-uploads.md) says.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### JavaScript example

```js
import { S3Client } from '@aws-sdk/client-s3';
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
import { randomUUID } from 'node:crypto';

const MAX_PHOTO_BYTES = 10 * 1024 * 1024;
const UPLOAD_WINDOW_SECONDS = 600;

export async function startPhotoUpload(s3, shopId) {
  const key = `quarantine/shops/${shopId}/${randomUUID()}`;
  const { url, fields } = await createPresignedPost(s3, {
    Bucket: 'example-shop-uploads',
    Key: key,
    Conditions: [['content-length-range', 1, MAX_PHOTO_BYTES], ['eq', '$Content-Type', 'image/jpeg']],
    Fields: { 'Content-Type': 'image/jpeg' },
    Expires: UPLOAD_WINDOW_SECONDS,
  });
  return { key, url, fields };
}
```

### Python example

```python
import uuid
import boto3

MAX_PHOTO_BYTES = 10 * 1024 * 1024
UPLOAD_WINDOW_SECONDS = 600

def start_photo_upload(s3, shop_id: str) -> dict:
    key = f"quarantine/shops/{shop_id}/{uuid.uuid4()}"
    post = s3.generate_presigned_post(
        Bucket="example-shop-uploads",
        Key=key,
        Fields={"Content-Type": "image/jpeg"},
        Conditions=[["content-length-range", 1, MAX_PHOTO_BYTES], {"Content-Type": "image/jpeg"}],
        ExpiresIn=UPLOAD_WINDOW_SECONDS,
    )
    return {"key": key, **post}
```

## Sources

- [Amazon S3 uploading objects with presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html)

## Related rules

- SCL-37 [Keep no state inside the app process](https://agentwranglers.se/playbook/scaling/stateless-app-servers/stateless-processes.md) in Scaling
- ECM-22 [Deliver downloads through short-lived links tied to the order](https://agentwranglers.se/playbook/ecommerce/orders-and-fulfilment/digital-downloads.md) in E-commerce and fulfilment
- MED-1 [Define each kind of upload once](https://agentwranglers.se/playbook/files-and-media/uploads/upload-kinds.md) in Files and media
- MED-43 [Confirm every upload on the server](https://agentwranglers.se/playbook/files-and-media/uploads/confirm-uploads.md) in Files and media
- APS-43 [Check every uploaded file](https://agentwranglers.se/playbook/application-security/requests-files-and-caches/safe-file-uploads.md) in Application security
- MED-27 [Count each owner's storage and enforce a quota](https://agentwranglers.se/playbook/files-and-media/quotas-and-cost/storage-quotas.md) in Files and media

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/files-and-media/uploads/presigned-uploads">Agent Wranglers files and media playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Define each kind of upload once](https://agentwranglers.se/playbook/files-and-media/uploads/upload-kinds.md)
- Next: [Make large uploads resumable](https://agentwranglers.se/playbook/files-and-media/uploads/resumable-uploads.md)
- [Uploads](https://agentwranglers.se/playbook/files-and-media/uploads.md)
