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.10.tar.gz (22.0 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.10-py3-none-any.whl (23.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: tiercache-0.1.10.tar.gz
  • Upload date:
  • Size: 22.0 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.10.tar.gz
Algorithm Hash digest
SHA256 2e3f883d49c8650ff7c6784a9232fd47de26b2bc54020885a6b416f3a0a1b06e
MD5 0225f4363e796a4440f1980195132cea
BLAKE2b-256 8cb826d75b69bfd0dde305a8278946c64925fac29c5d977d3cd35469d4747216

See more details on using hashes here.

File details

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

File metadata

  • Download URL: tiercache-0.1.10-py3-none-any.whl
  • Upload date:
  • Size: 23.3 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.10-py3-none-any.whl
Algorithm Hash digest
SHA256 fa6df3bed16bb51f13d2c8413bfdca0c1cf3563f0109cc71e78df92d872b185d
MD5 43ba992678c2e52c9b23be8ef0d8f7aa
BLAKE2b-256 97e633d4835137c87f7447b91130afa2c8354c09850366a3c1c0354f4538171c

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