Skip to main content

RAM-first three-tier cache with swappable backends

Project description

TierCache

RAM-first three-tier cache for Python. Designed to keep your SSD/HDD out of the hot path.

pip install tiercache

How it works

Every request walks down the tier chain until a hit is found:

GET request
  │
  ├─ Hot cache  (RAM, 2GB, 4h TTL)   ──── HIT → serve, reset TTL
  │                                   MISS ↓
  ├─ Cold cache (RAM, 10GB, 24h TTL) ──── HIT → promote to hot → serve
  │                                   MISS ↓
  └─ Dry cache  (Disk / S3 / MongoDB) ─── HIT → promote to hot → serve
                                      MISS → return None (fetch from origin)

SET request
  └─ Writes to hot only (zero disk I/O)
       │
       └─ When hot evicts or expires → auto-demote to dry (failsafe, background)

Both hot and cold live entirely in RAM. Dry is only hit on a true cache miss. After a server restart, the first GET recovers each item from dry back into hot.


Installation

# Base (RAM + local filesystem + SQLite tracking)
pip install tiercache

# With Memcached backends (multi-process / multi-server)
pip install "tiercache[memcached]"

# With Redis tracking
pip install "tiercache[redis]"

# With S3 dry cache
pip install "tiercache[s3]"

# With MongoDB
pip install "tiercache[mongodb]"

# With PostgreSQL tracking
pip install "tiercache[postgres]"

# Everything
pip install "tiercache[all]"

Quick start

From a config file

from tiercache import CacheManager

cache = CacheManager.from_config("tiercache.yaml")

# Async (FastAPI, aiohttp, Sanic)
value = await cache.get("my-key")
await cache.set("my-key", data)

# Sync (Flask, Django)
value = cache.get_sync("my-key")
cache.set_sync("my-key", data)

In code

from tiercache import CacheManager
from tiercache.backends.ram import RamBackend
from tiercache.backends.dry.local import LocalBackend
from tiercache.tracking.sqlite import SQLiteTracking

cache = CacheManager(
    hot=RamBackend(ttl_seconds=14400, max_size_bytes=2 * 1024**3),
    cold=RamBackend(ttl_seconds=86400, max_size_bytes=10 * 1024**3),
    dry=LocalBackend(base_path="/var/cache/myapp/dry", max_size_bytes=100 * 1024**3),
    tracking=SQLiteTracking(path="/var/cache/myapp/index.db"),
)

Configuration

# tiercache.yaml

hot_cache:
  backend: ram          # ram | memcached
  ttl_hours: 4
  max_size_gb: 2

cold_cache:
  backend: ram          # ram | memcached
  ttl_hours: 24
  max_size_gb: 10

dry_cache:
  backend: local        # local | s3 | mongodb
  max_size_gb: 100
  path: /var/cache/myapp/dry

tracking:
  backend: sqlite       # sqlite | redis | postgres | mongodb

# Optional: TTL rules by tag
ttl_rules:
  - tag: { type: thumbnail }
    hot_ttl_hours: 1
    cold_ttl_hours: 6
  - tag: { type: raw }
    hot_ttl_hours: 8
    cold_ttl_hours: 48

Memcached (multi-process / multi-server)

hot_cache:
  backend: memcached
  ttl_hours: 4
  max_size_gb: 2

cold_cache:
  backend: memcached
  ttl_hours: 24
  max_size_gb: 10

memcached:
  host: localhost
  port: 11211

S3 dry cache

dry_cache:
  backend: s3

s3:
  endpoint_url: https://s3.amazonaws.com   # or MinIO, Cloudflare R2, etc.
  bucket: my-cache-bucket
  access_key: ...
  secret_key: ...

MongoDB dry cache + tracking

dry_cache:
  backend: mongodb

tracking:
  backend: mongodb

mongodb:
  uri: mongodb://localhost:27017
  database: tiercache

Redis tracking

tracking:
  backend: redis

redis:
  host: localhost
  port: 6379
  db: 0

API

# Fetch a value (returns None on miss)
value = await cache.get("key")

# Store a value using tier default TTL
await cache.set("key", data)

# Override TTL for this key only
await cache.set("key", data, ttl_hours=2)

# Tag-based TTL (matched against ttl_rules in config)
await cache.set("key", data, tags={"type": "thumbnail"})

# Delete from all tiers
await cache.delete("key")

# Flush a specific tier or all
await cache.flush(tier="hot")   # hot | cold | dry | all

# Hit/miss stats + tier sizes
stats = await cache.stats()
# {
#   "hot_hits": 120, "cold_hits": 30, "dry_hits": 5, "misses": 2,
#   "hot_size_bytes": 1048576, "cold_size_bytes": 0, "dry_size_bytes": 4096
# }

# Sync equivalents (Flask, Django)
cache.get_sync("key")
cache.set_sync("key", data, ttl_hours=2, tags={"type": "thumbnail"})
cache.delete_sync("key")
cache.flush_sync(tier="hot")
cache.stats_sync()

# Always close on shutdown
await cache.close()

TTL priority (highest wins)

Priority Example
1. Per-key override cache.set("k", v, ttl_hours=1)
2. Tag rule cache.set("k", v, tags={"type": "thumbnail"}) → matched in config
3. Tier default hot_cache.ttl_hours in yaml
4. Global default hot: 4h, cold: 24h, dry: no expiry

Backends

Tier Backend Notes
Hot / Cold ram In-process, single server
Hot / Cold memcached Shared pool, multi-process/server
Dry local Local filesystem, SSD/HDD
Dry s3 AWS S3, MinIO, Cloudflare R2
Dry mongodb GridFS + native TTL indexes
Tracking sqlite Zero deps, single machine
Tracking redis In-memory, fast, recommended
Tracking postgres Production relational
Tracking mongodb Flexible schema, TTL indexes

Example HTTP app (FastAPI)

pip install fastapi uvicorn

# Single process (RAM)
uvicorn example.app:app --port 8989

# Multi-process (Memcached — shared cache across all workers)
SMARTCACHE_CONFIG=example/config_memcached.yaml \
uvicorn example.app:app --workers 4 --port 8989
# Store an image
curl -X PUT "http://localhost:8989/cache/photo.png?tag_type=thumbnail" \
    -H "Content-Type: image/png" \
    --data-binary @photo.png

# Fetch it (opens directly in browser)
curl "http://localhost:8989/cache/photo.png" -o out.png

# Stats
curl "http://localhost:8989/stats"

Why not just use Redis for everything?

Redis is great but it is a network service — every cache hit is a round trip. TierCache's RAM backend (ram) stores values directly in the Python process memory, making hot-path lookups microsecond-range with zero network overhead.

Use memcached when you need shared cache across multiple processes or servers. Use redis for tracking metadata (tiny footprint, fast, persistent).


License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tiercache-0.1.13.tar.gz (22.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tiercache-0.1.13-py3-none-any.whl (24.2 kB view details)

Uploaded Python 3

File details

Details for the file tiercache-0.1.13.tar.gz.

File metadata

  • Download URL: tiercache-0.1.13.tar.gz
  • Upload date:
  • Size: 22.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for tiercache-0.1.13.tar.gz
Algorithm Hash digest
SHA256 8356a385d094f11c4ba45be928e430579328c8f8d47e84372f07ea0604f1ad06
MD5 2bfb727e94728d7dd8de379881a5c2d9
BLAKE2b-256 455efd0d1228fa4b64b7284fef66d508ce0fc3308bb54763a53913d9623ed8cb

See more details on using hashes here.

File details

Details for the file tiercache-0.1.13-py3-none-any.whl.

File metadata

  • Download URL: tiercache-0.1.13-py3-none-any.whl
  • Upload date:
  • Size: 24.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for tiercache-0.1.13-py3-none-any.whl
Algorithm Hash digest
SHA256 ea2825e201bb8789e9eabbf6961daf1da6d7d9d1088580e7dcc428eb80a205fd
MD5 e4b0de7e54944e3b0f742c1478cafffe
BLAKE2b-256 8ffc549fa3446a96266cd16168dfc03fb37177fe830bd8fcc3ce75edd89a8e53

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page