# Cache Control

TAG supports RFC 7234-compliant cache revalidation. Clients can control caching behavior using standard `Cache-Control` headers, and TAG reports cache status via the `X-Cache` response header.

## X-Cache header reference[​](#x-cache-header-reference "Direct link to X-Cache header reference")

| Value         | Meaning                                                                          |
| ------------- | -------------------------------------------------------------------------------- |
| `HIT`         | Served from cache (includes revalidation that confirmed the object is unchanged) |
| `MISS`        | Not in cache, fetched from upstream and now cached                               |
| `REVALIDATED` | Revalidated with upstream, object changed, new content returned                  |
| `BYPASS`      | Cache bypassed entirely (client requested `no-store`)                            |
| `DISABLED`    | Caching is disabled server-side (`TAG_CACHE_DISABLED=true`)                      |

## Object size threshold[​](#object-size-threshold "Direct link to Object size threshold")

TAG caches objects up to a configurable size limit (`cache.size_threshold`, default 1 GiB). Objects larger than this threshold bypass the cache entirely: they are fetched from Tigris and streamed directly to the client without being written to disk.

Configure the threshold via the config file:

```
cache:

  size_threshold: 5368709120 # 5 GiB
```

Responses that bypass the threshold return `X-Cache: MISS` and are not stored. Subsequent requests for the same large object will always go to Tigris.

## Cache eviction[​](#cache-eviction "Direct link to Cache eviction")

Cached objects are evicted through two mechanisms:

* **TTL expiry** — Objects expire after the configured TTL (default 24 hours). The next request for an expired object triggers revalidation with Tigris.
* **LRU eviction** — TAG tracks disk usage and when it approaches the `max_disk_usage_bytes` limit, the least recently used objects are evicted to keep disk usage below the watermark. Reads and writes continue normally — the LRU mechanism works proactively in the background.

If `max_disk_usage_bytes` is `0` (the default), LRU eviction is disabled and objects are only removed by TTL expiry or explicit invalidation.

## Automatic cache invalidation[​](#automatic-cache-invalidation "Direct link to Automatic cache invalidation")

TAG automatically invalidates cached objects when they are modified through TAG:

* **PutObject** — cache entry deleted before forwarding the upload
* **DeleteObject** — cache entry deleted before forwarding the delete
* **DeleteObjects** (bulk) — cache entries deleted for all keys in the request
* **CopyObject** — cache entry deleted for the destination key

Objects modified directly on Tigris (bypassing TAG) remain in cache until they expire (default TTL: 24 hours) or are revalidated via `Cache-Control: no-cache`.

## Force revalidation[​](#force-revalidation "Direct link to Force revalidation")

Send `Cache-Control: no-cache` or `Cache-Control: max-age=0` to force TAG to check with upstream before serving a cached object. TAG sends a conditional request using the cached ETag. If the object hasn't changed, upstream returns 304 and TAG serves from cache (`X-Cache: HIT`). If changed, TAG streams the new content (`X-Cache: REVALIDATED`).

```
import boto3

from botocore.config import Config



s3 = boto3.client(

    "s3",

    endpoint_url="http://localhost:8080",

    config=Config(s3={"addressing_style": "path"}),

)



# Add Cache-Control header to HEAD requests

def add_no_cache(params, **kwargs):

    params["headers"]["Cache-Control"] = "no-cache"



s3.meta.events.register("before-sign.s3.HeadObject", add_no_cache)



response = s3.head_object(Bucket="my-bucket", Key="my-key")

print(response["ResponseMetadata"]["HTTPHeaders"].get("x-cache"))

# → HIT (object unchanged) or REVALIDATED (object changed)
```

If the revalidation request to upstream fails, TAG serves the stale cached copy as a fallback.

## Bypass cache[​](#bypass-cache "Direct link to Bypass cache")

Send `Cache-Control: no-store` to skip the cache entirely. TAG forwards the request directly to upstream and does not cache the response.

```
def add_no_store(params, **kwargs):

    params["headers"]["Cache-Control"] = "no-store"



s3.meta.events.register("before-sign.s3.HeadObject", add_no_store)



response = s3.head_object(Bucket="my-bucket", Key="my-key")

print(response["ResponseMetadata"]["HTTPHeaders"].get("x-cache"))

# → BYPASS
```

## Verifying cache behavior[​](#verifying-cache-behavior "Direct link to Verifying cache behavior")

Check the `X-Cache` header to verify caching:

```
import boto3

from botocore.config import Config



s3 = boto3.client(

    "s3",

    endpoint_url="http://localhost:8080",

    config=Config(s3={"addressing_style": "path"}),

)



# First request — fetched from Tigris

response = s3.head_object(Bucket="my-bucket", Key="my-key")

print(response["ResponseMetadata"]["HTTPHeaders"].get("x-cache"))

# → MISS



# Second request — served from cache

response = s3.head_object(Bucket="my-bucket", Key="my-key")

print(response["ResponseMetadata"]["HTTPHeaders"].get("x-cache"))

# → HIT
```
