---
title: S3 (fetch)
description: AWS S3 and any S3-compatible bucket over a SigV4-signed fetch engine - no @aws-sdk/* packages, built for Cloudflare Workers and other edge runtimes.
related:
  - /guides/cloudflare-workers-r2-storage
---

## Installation

This adapter has no extra peer dependencies. It signs requests with [aws4fetch](https://github.com/mhart/aws4fetch) (~2.5&nbsp;KB gzipped, Web Crypto only), so nothing from `@aws-sdk/*` is installed or bundled:

```package-install
files-sdk
```

## When to use it

[`files-sdk/s3`](/docs/adapters/s3) and the S3-compatible wrappers built on it ([MinIO](/docs/adapters/minio), [Tigris](/docs/adapters/tigris), [Wasabi](/docs/adapters/wasabi), ...) import `@aws-sdk/client-s3`. On a browser-targeted bundle that SDK's XML parser needs a `DOMParser`, which Cloudflare Workers don't provide - every `list()` or error-body parse throws `DOMParser is not defined` at runtime. `s3Fetch()` is the same engine that backs [`r2()`'s fetch client](/docs/adapters/r2#lightweight-fetch-client), pointed at an endpoint of your choosing, for exactly those runtimes.

Outside Workers and edge runtimes prefer `files-sdk/s3`: it carries the full surface (multipart and resumable uploads, batched deletes, byte-level progress, the AWS credential chain) that this engine trades away for size.

## Usage

```ts lineNumbers
import { Files } from "files-sdk";
import { s3Fetch } from "files-sdk/s3-fetch";

export default {
  async fetch(request: Request, env: Env) {
    const files = new Files({
      adapter: s3Fetch({
        bucket: "uploads",
        endpoint: "https://s3.us-east-1.amazonaws.com",
        region: "us-east-1",
        accessKeyId: env.AWS_ACCESS_KEY_ID,
        secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
      }),
    });
    // ...
  },
};
```

Credentials are static: pass `accessKeyId` + `secretAccessKey` (and `sessionToken` for temporary credentials), or set `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` where a `process.env` exists. `AWS_SESSION_TOKEN` is only read when both keys also come from the environment, so keys you pass explicitly never pick up a token that belongs to another identity (a Lambda role or SSO session, say). There is no AWS credential chain - no IAM role, shared profile, or SSO resolution - because that chain lives in the SDK this adapter avoids.

### Addressing

Requests default to virtual-hosted style (`https://<bucket>.<endpoint host>/<key>`). Set `forcePathStyle: true` for services without per-bucket DNS - MinIO, LocalStack, most self-hosted gateways - and for bucket names containing dots, which break virtual-hosted TLS:

```ts lineNumbers
s3Fetch({
  bucket: "uploads",
  endpoint: "http://localhost:9000",
  forcePathStyle: true,
  accessKeyId: "minioadmin",
  secretAccessKey: "minioadmin",
});
```

Get this wrong against such a service and the first request fails with a `NotFound` whose message names the wrong bucket: with no per-bucket DNS the server reads the key's first folder as the bucket name. The error message points at `forcePathStyle` when that happens, but only on responses that carry an error body. A `HEAD` response has none, so `head()` rejects with a bare `NotFound` and `exists()` returns `false` for every key. If `exists()` reports everything missing on a new setup, check `forcePathStyle` first. For MinIO specifically, prefer [`minio({ client: "fetch" })`](/docs/adapters/minio#lightweight-fetch-client) - the same engine with path style, region, and error labels already defaulted.

Only the endpoint's origin is used; a path component is dropped. Front the service with a hostname rather than a route prefix.

## Trade-offs

Same coverage and limits as the R2 fetch client: `upload`, `download` (including [ranges](/docs/api/download#byte-ranges)), `head`, `exists`, `delete`, `list` (including `delimiter` folding), server-side `copy`, presigned `url()`, and `signedUploadUrl()` are all covered. Against `files-sdk/s3`:

- `ReadableStream` bodies are buffered in memory before a single PUT (a lone PUT needs a `Content-Length`, and single-request uploads cap at 5&nbsp;GB). Inside a Worker that buffering counts against the isolate's memory limit.
- `multipart` and resumable (`control`) uploads throw instead of engaging the S3 multipart API.
- Bulk deletes fan out as per-key `delete()` calls instead of batched `DeleteObjects` requests. On Workers each call is a subrequest, so chunk large bulk deletes to stay under the per-invocation subrequest cap.
- `signedUploadUrl()` returns a presigned PUT; `maxSize` and a positive `minSize` throw because enforcing either needs a presigned POST policy this engine doesn't implement. Enforce upload caps at your application gateway instead.
- Keys containing `.` or `..` path segments are rejected - URL normalization would silently sign a request for a different key.
- Byte-level `onProgress` reporting falls back to the SDK's generic reporting.
- No provider-native [conditional operations](/docs/conditional-operations).
- `files.raw` is the aws4fetch `AwsClient`, not an `S3Client`.

S3's own [limits](/docs/adapters/s3#limits) apply here too: presigned URLs live at most 7 days (a longer `expiresIn` throws), and `copy()` is a single `CopyObject` request up to 5 GiB, past which it switches to a server-side multipart copy (`UploadPartCopy`).

## Options

<AutoTypeTable
  path="../../packages/files-sdk/src/s3-fetch/index.ts"
  name="S3FetchAdapterOptions"
/>

## Compatibility

| Method | Status | Notes |
| --- | :-: | --- |
| `upload` | ⚠️ | Single PUT - `ReadableStream` bodies are buffered in memory first, and `multipart` / resumable `control` uploads throw. |
| `download` | ✅ |  |
| `delete` | ✅ | Bulk deletes fan out per key (no batched `DeleteObjects`). |
| `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `contentType` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. |
| `search` | ✅ |  |
| `head` | ✅ |  |
| `exists` | ✅ |  |
| `copy` | ✅ |  |
| `url` | ✅ |  |
| `signedUploadUrl` | ⚠️ | PUT URL only - `maxSize` and a positive `minSize` throw (no presigned POST policy). |
| conditional operations | ❌ | Not advertised on this engine. |
