# SXO ETag and Caching: How Hashed vs Non-Hashed Assets Are Handled

> Learn how SXO manages ETag and caching for hashed vs non-hashed assets. Discover immutable year-long caches for hashed files and five-minute caches for others, optimizing performance.

- Repository: [Víctor García/sxo](https://github.com/gc-victor/sxo)
- Tags: performance
- Published: 2026-03-02

---

**SXO assigns immutable year-long caches to hashed assets and five-minute caches to non-hashed files, generating weak ETags from file metadata for conditional requests while deliberately omitting ETags on pre-compressed variants to prevent cache mismatches.**

The gc-victor/sxo repository implements a deterministic static file serving strategy that differentiates cache behavior based on filename hashing. Understanding how SXO handles ETag and caching for hashed vs non-hashed assets reveals how the framework optimizes long-term asset storage while keeping dynamic content fresh through precise header control.

## Cache-Control Policies for Asset Types

### Hashed Assets: Immutable Long-Term Caching

Files containing content hashes in their filenames receive the `CACHE_IMMUTABLE` directive defined in [`src/js/server/shared/cache.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/shared/cache.js). This header value is `public, max-age=31536000, immutable`, instructing browsers to cache the resource for one year without revalidation attempts. The `isHashedAsset()` function in [`src/js/server/shared/path.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/shared/path.js) detects these files using a regex that matches hexadecimal strings of eight or more characters or esbuild-style base36 hash segments.

### Non-Hashed Assets: Short-Lived Caching

Standard files without hash segments in their names receive the `CACHE_SHORT` constant (`public, max-age=300`), establishing a five-minute cache lifetime. This ensures that unversioned resources remain fresh while still benefiting from client-side caching between updates.

## ETag Generation and Validation Strategy

### Weak ETag Construction from File Stats

The `weakEtagFromStat()` utility in [`src/js/server/shared/cache.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/shared/cache.js) generates validators by combining the file's size and modification time into a hexadecimal string formatted as `W/"sizeHex-mtimeHex"`. This helper normalizes the stat object interface to support both Node.js (`mtimeMs` property) and Deno (`mtime` Date object) environments.

```javascript
// src/js/server/shared/cache.js
export function weakEtagFromStat(stat) {
  let mtimeMs = 0;
  if (typeof stat.mtimeMs === "number") {
    mtimeMs = stat.mtimeMs;
  } else if (stat.mtime instanceof Date) {
    mtimeMs = stat.mtime.getTime();
  }
  return weakEtag(stat.size, mtimeMs);
}

```

### ETag Omission for Compressed Variants

When serving pre-compressed `.br` or `.gz` file variants, SXO explicitly sets the ETag header to `null`. Since the response depends on the client's `Accept-Encoding` negotiation, including an ETag could cause cache collisions where a browser matches an uncompressed representation against a compressed request, or vice versa.

## Implementation Flow in the Static Handler

The request handling logic resides in [`src/js/server/shared/static-handler.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/shared/static-handler.js) and executes the following steps:

1. **Asset Classification**: Extract the basename using `getBasename(absPath)` and test `isHashedAsset(basename)` to select between `CACHE_IMMUTABLE` and `CACHE_SHORT`.

2. **File Statistics**: Obtain metadata via `fileReader.stat(sendPath)` to access size and modification time.

3. **Conditional Validation**: Compare the client's `If-None-Match` header against the computed weak ETag. When matched, return **304 Not Modified** with the current `Cache-Control` and `ETag` headers.

4. **Header Assembly**: Construct the response with `Content-Type`, `Cache-Control`, and `ETag` (when applicable). Add `Vary: Accept-Encoding` for negotiated responses.

```javascript
// src/js/server/shared/static-handler.js
const basename = getBasename(absPath);
const effectiveCacheControl = cacheControl ?? (isHashedAsset(basename) ? CACHE_IMMUTABLE : CACHE_SHORT);

const stat = await fileReader.stat(sendPath);
const etag = contentEncoding ? null : weakEtagFromStat(stat);

if (etag) {
  const ifNoneMatch = request.headers.get("If-None-Match");
  if (ifNoneMatch === etag) {
    return new Response(null, {
      status: HTTP_STATUS_NOT_MODIFIED,
      headers: {
        "Cache-Control": effectiveCacheControl,
        ETag: etag,
      },
    });
  }
}

const headers = new Headers({
  "Content-Type": mimeType,
  "Cache-Control": effectiveCacheControl,
});
if (etag) headers.set("ETag", etag);
if (!skipCompression) headers.set("Vary", "Accept-Encoding");

```

## Summary

- **Hashed assets** receive one-year immutable caching via `CACHE_IMMUTABLE` in [`src/js/server/shared/cache.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/shared/cache.js), triggered by `isHashedAsset()` detection in [`src/js/server/shared/path.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/shared/path.js).
- **Non-hashed assets** use short-lived five-minute caching through the `CACHE_SHORT` constant.
- **Weak ETags** are generated from file size and mtime using `weakEtagFromStat()` to support conditional 304 responses.
- **Compressed variants** intentionally omit ETags to prevent encoding-specific cache mismatches.
- **Conditional requests** are validated against the weak ETag in [`src/js/server/shared/static-handler.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/shared/static-handler.js) before serving file contents.

## Frequently Asked Questions

### How does SXO detect whether a file contains a content hash?

SXO uses the `isHashedAsset()` function in [`src/js/server/shared/path.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/shared/path.js), which applies the regex `/(?:^|\.|-)(?:[a-f0-9]{8,}|[A-Z0-9]{8})(?:\.|$)/` to detect hexadecimal strings of eight or more characters or base36 hash segments within the filename.

### Why does SXO serve compressed files without ETag headers?

The static handler omits ETags for pre-compressed `.br` and `.gz` variants because the same logical resource serves different byte representations based on `Accept-Encoding` negotiation. Including an ETag could cause browsers to incorrectly match a compressed representation with an uncompressed request during cache validation.

### What happens when a client sends a matching If-None-Match header?

When the client's `If-None-Match` value matches the weak ETag generated by `weakEtagFromStat()`, SXO returns an HTTP **304 Not Modified** response containing the `Cache-Control` and `ETag` headers but no response body, eliminating redundant data transfer for unchanged resources.

### What is the format of SXO's weak ETags?

The `weakEtag()` helper produces tags in the format `W/"sizeHex-mtimeHex"`, where the file size and modification timestamp are converted to hexadecimal strings and concatenated to create a unique validator for the resource state.