> ## Documentation Index
> Fetch the complete documentation index at: https://wemstudios.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Response Caching, Compression, and ETags in Blaze Workers

> Cache responses with Cloudflare's Cache API and compress them with gzip or deflate using Blaze's built-in cache(), compress(), and etag() middleware.

Blaze ships two performance-focused middleware modules: `cache()` integrates with **Cloudflare's Cache API** to store and serve responses at the edge, and `compress()` shrinks response bodies using the Web-standard **`CompressionStream`** API. A third module, `etag()`, complements both by generating cache validators that let browsers skip downloads entirely when content hasn't changed.

## Cache middleware

`cache()` checks `caches.default` — the Cloudflare Cache API available in every Worker — before calling downstream handlers. On a cache hit, it returns the stored response immediately. On a miss, it lets the request proceed normally and stores the response in the cache for future requests.

```typescript theme={null}
import { cache } from 'blaze/middleware/cache'
```

<Note>
  The Cloudflare Cache API is scoped to the **datacenter** that handles each request. A response cached in Frankfurt is not automatically available in Singapore. For globally consistent caching, combine `cache()` with a KV-backed strategy or use Cloudflare's Cache Rules in your dashboard.
</Note>

### `CacheOptions` reference

| Option         | Type       | Default   | Description                                                                        |
| -------------- | ---------- | --------- | ---------------------------------------------------------------------------------- |
| `maxAge`       | `number`   | `60`      | `max-age` value in seconds for the generated `Cache-Control` header.               |
| `sMaxAge`      | `number`   | —         | `s-maxage` for shared (CDN) caches. Added to `Cache-Control` alongside `maxAge`.   |
| `methods`      | `string[]` | `['GET']` | HTTP methods to cache. Non-matching methods bypass the cache entirely.             |
| `cacheControl` | `string`   | —         | Explicit `Cache-Control` header value. When set, overrides `maxAge` and `sMaxAge`. |

### Caching GET responses

```typescript theme={null}
import { createApp } from 'blaze'
import { cache } from 'blaze/middleware/cache'

const app = createApp<Env>()

// Cache all GET responses for 5 minutes
app.use(cache({ maxAge: 300 }))

app.get('/api/products', async (req, res) => {
  const { results } = await req.env.DB
    .prepare('SELECT * FROM products')
    .all()
  res.json(results)
})

export default { fetch: app.fetch }
```

The middleware respects your existing `Cache-Control` headers: if your handler already sets `Cache-Control`, `cache()` stores the response as-is without overriding it. Only responses with status codes in the `2xx`–`3xx` range are stored.

### Cache-Control passthrough

```typescript theme={null}
// Apply cache() globally, but let individual routes control their own TTL
app.use(cache({ maxAge: 60 })) // fallback: 1 minute

app.get('/api/products', async (req, res) => {
  const products = await loadProducts(req.env.DB)

  // This header takes precedence — cache() won't overwrite it
  res.header('Cache-Control', 'public, max-age=3600, s-maxage=86400')
  res.json(products)
})

app.get('/api/user/profile', async (req, res) => {
  const profile = await loadProfile(req.env.DB, req.user!.sub)

  // Private data — prevent caching entirely
  res.header('Cache-Control', 'private, no-store')
  res.json(profile)
})
```

## ETag middleware

`etag()` generates a weak ETag for each response body using a fast **djb2 hash** (no crypto overhead) and handles conditional requests via `If-None-Match` → `304 Not Modified`. This means repeat visitors skip the download entirely when the content hasn't changed.

```typescript theme={null}
import { etag } from 'blaze/middleware/etag'
```

### `EtagOptions`

| Option | Type      | Default | Description                                                                                                        |
| ------ | --------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
| `weak` | `boolean` | `true`  | Generate weak ETags (`W/"…"`). Weak ETags allow semantic equivalence; strong ETags require byte-for-byte identity. |

```typescript theme={null}
import { etag } from 'blaze/middleware/etag'

app.use(etag()) // adds ETag header and handles If-None-Match → 304

app.get('/api/config', async (req, res) => {
  const config = await req.env.KV.get('app-config', { type: 'json' })
  res.json(config) // ETag computed from the JSON string; 304 if unchanged
})
```

`etag()` patches `res.json()`, `res.send()`, and `res.html()` to intercept the response body before it's sent. If the computed ETag matches the `If-None-Match` request header, it short-circuits to a `304 Not Modified` with no body.

## Compress middleware

`compress()` uses the Web-standard `CompressionStream` API to pipe response bodies through gzip or deflate compression. It checks the `Accept-Encoding` request header first, skips already-encoded responses (bodies with a `Content-Encoding` header), and skips responses below a configurable byte threshold.

```typescript theme={null}
import { compress } from 'blaze/middleware/compress'
```

### `CompressOptions`

| Option      | Type                      | Default               | Description                                                                                                             |
| ----------- | ------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `threshold` | `number`                  | `1024`                | Minimum response body size in bytes. Smaller responses are not compressed.                                              |
| `encodings` | `('gzip' \| 'deflate')[]` | `['gzip', 'deflate']` | Accepted encodings in priority order. The first encoding that appears in the client's `Accept-Encoding` header is used. |

```typescript theme={null}
import { compress } from 'blaze/middleware/compress'

// Compress responses larger than 512 bytes, preferring gzip
app.use(compress({ threshold: 512, encodings: ['gzip'] }))

app.get('/api/large-dataset', async (req, res) => {
  const rows = await req.env.DB.prepare('SELECT * FROM events').all()
  res.json(rows.results) // compressed automatically if client accepts gzip
})
```

When compression is applied, `compress()` sets `Content-Encoding` to the chosen algorithm, removes `Content-Length` (since the compressed size differs), and appends `Accept-Encoding` to the `Vary` header so caches store separate copies for compressed and uncompressed clients.

## Combining cache + compress + etag

Layer all three middleware together for maximum performance. Order matters: compress the response first, then generate the ETag from the compressed bytes, then store the compressed + tagged response in the cache.

```typescript theme={null}
import { createApp } from 'blaze'
import { compress } from 'blaze/middleware/compress'
import { etag } from 'blaze/middleware/etag'
import { cache } from 'blaze/middleware/cache'

type Env = { DB: D1Database }
const app = createApp<Env>()

// Stack order: compress → etag → cache → routes
// compress runs first so the cache stores the already-compressed body.
// etag runs after compress so the ETag reflects the compressed payload.
// cache runs last (closest to the route) so it intercepts the final response.
app.use(compress({ threshold: 512 }))
app.use(etag())
app.use(cache({ maxAge: 300, sMaxAge: 3600 }))

app.get('/api/catalog', async (req, res) => {
  const { results } = await req.env.DB.prepare('SELECT * FROM catalog').all()
  res.json(results)
})

export default { fetch: app.fetch }
```

With this stack, a warm request from a browser with a matching ETag results in:

1. `etag()` checks the `If-None-Match` header against its computed ETag — sends `304 Not Modified` immediately if unchanged.
2. The browser uses its local copy, paying zero bandwidth.

A warm request without a matching ETag (cache already populated) results in:

1. `cache()` checks `caches.default` — cache hit, returns the stored compressed response immediately.
2. `etag()` sets the `ETag` header on the outgoing response.
3. The browser receives the compressed response and caches the ETag for future requests.

A cold request (cache miss, no prior ETag) results in:

1. `cache()` misses — calls `next()` to run the route handler.
2. The route handler returns JSON.
3. `compress()` streams the body through `CompressionStream`.
4. `etag()` hashes the body, sets the `ETag` header.
5. `cache()` stores the final compressed response in `caches.default`.
6. The browser receives the compressed, tagged response.
