# Expiry on Read

This page explains how TTL is stored, how each tier detects expiry on read, and what the background sweep covers.

## How TTL Is Stored

`Set` converts the TTL to whole seconds in `Cache.TTL` and records the Unix second of the write in `Cache.Timestamp` (`set.go:16-25`).

| `ttl` argument to `Set` | `Cache.TTL` | Result |
|---|---|---|
| `10 * time.Minute` | `600` | Expires after 600 seconds |
| `0` or negative | `0` | Never expires |
| `500 * time.Millisecond` | `0` (truncated) | **Never expires** |
| `1500 * time.Millisecond` | `1` | Expires after 1 second |

A TTL under one second truncates to 0, which means no expiry; Redis is written with 0 as well (no expiry).

## Expiry Check

`isExpired` (`unit.go:26-31`): `TTL ≤ 0` never expires; otherwise the entry is expired when `now > Timestamp + TTL`, at one-second precision.

## Handling per Tier

| Location | On an expired entry | Source |
|---|---|---|
| Normal-mode memory read | Delete the memory entry and the local file, return `Not found` | `get.go:28-33` |
| Fallback-mode memory read | Delete only the memory entry, return `Not found` | `get.go:68-72` |
| Fallback-mode file read | Delete the file, return `Not found` | `get.go:95-100` |
| Recovery resync | Expired entries are not written to Redis | `sync.go:109` |
| Redis itself | Handled by Redis key expiry | — |

## Background Sweep

`New` starts a ticker that fires every 30 seconds (`sync.go:135-154`), iterates the memory cache, and deletes expired entries together with their local files.

| Item | Behavior |
|---|---|
| Scope | Only keys in the memory cache; local files never loaded into memory are not swept |
| Interval | Fixed at 30 seconds, not configurable |
| Stopping | `Close` does not stop this ticker |
| Skip condition | `isRecovering` is checked once when `New` runs; if true the sweep never starts |

## Effect on Reads

`Get` never returns expired data: every tier checks expiry before returning. The background sweep only affects memory and disk usage, not read results.
