Skip to content

About

Backend-agnostic caching for Python — intent-based decorators with circuit breaker, distributed locking, Prometheus metrics, and optional zero-knowledge AES-256-GCM encryption, on a Rust-powered core. Zero-config L1 in-memory; scales to Redis, Memcached, File, or CacheKit Cloud.

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Latest commit

 

History

488 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cachekit

Python caching, batteries included

Backend-agnostic caching with intent-based decorators — circuit breaker, distributed locking, Prometheus metrics, and optional zero-knowledge encryption on a Rust-powered core. Zero config to start, any backend when you scale.

PyPI Version Python Versions codecov License: MIT


Note

Status: beta — CacheKit is in closed beta ahead of 1.0. APIs are stabilising; minor breaking changes may still occur between 0.x releases.

🐛 Found a bug or a rough edge? Please open an issue — your feedback directly shapes the path to 1.0.


Why cachekit?

Simple to use, works out of the box.

from cachekit import cache

@cache
def expensive_function():
    return fetch_data()

That's it. You get:

Feature Description
Circuit breaker Prevents cascading failures
Distributed locking Multi-pod safety
Prometheus metrics Built-in observability
MessagePack serialization Efficient with optional compression
Zero-knowledge encryption Client-side AES-256-GCM

Quick Start

Installation

pip install cachekit

Or with uv (recommended):

uv add cachekit

Setup (choose a backend)

cachekit exposes one decorator API over a pluggable backend abstraction. Pick the backend that fits your infrastructure — they're peers behind the same @cache API:

Backend Best for Select with
Redis Self-hosted, full control REDIS_URL / CACHEKIT_REDIS_URL
CachekitIO Managed, zero-ops (beta) CACHEKIT_API_KEY
Memcached High-throughput, existing infra CACHEKIT_MEMCACHED_SERVERS
File / L1-only Local dev, tests, no external deps CACHEKIT_FILE_CACHE_DIR / backend=None
# Run Redis locally or use your existing infrastructure
export REDIS_URL="redis://localhost:6379"
from cachekit import cache

@cache  # Auto-detects backend (defaults to Redis at localhost)
def expensive_api_call(user_id: int):
    return fetch_user_data(user_id)

Tip

No Redis? No worries! Use @cache(backend=None) for L1-only in-memory caching, like lru_cache, but with all the bells and whistles.

More Backends

CachekitIO — Managed SaaS (Beta)
import os
from cachekit import cache

# Set your CachekitIO API key
# export CACHEKIT_API_KEY="your-api-key"  # pragma: allowlist secret

@cache.io()  # Uses CachekitIO SaaS backend — no Redis to manage
def expensive_api_call(user_id: int):
    return fetch_user_data(user_id)

cachekit.io is in closed beta — request access to get started.

How fresh a cachekit.io read is, and how long a deleted project's data stays readable: Consistency and Deletion.

Memcached — Optional
from cachekit import cache
from cachekit.backends.memcached import MemcachedBackend

# pip install cachekit[memcached]

backend = MemcachedBackend()  # Defaults to 127.0.0.1:11211

@cache(backend=backend)
def expensive_api_call(user_id: int):
    return fetch_user_data(user_id)

Requires: pip install cachekit[memcached] or uv add cachekit[memcached]


CachekitIO Cloud (Beta) Managed caching with zero infrastructure. L1+L2 caching, circuit breaker, and automatic failover — no Redis to manage. cachekit.io is in closed beta — request access to get started.


Intent-Based Optimization

cachekit provides preset configurations for different use cases:

# Speed-critical: trading, gaming, real-time
@cache.minimal
def get_price(symbol: str):
    return fetch_price(symbol)

# Reliability-critical: payments, APIs
@cache.production
def process_payment(amount):
    return payment_gateway.charge(amount)

# Security-critical: PII, medical, financial (needs a key: master_key= or CACHEKIT_MASTER_KEY)
@cache.secure(master_key=secret_key)
def get_user_profile(user_id: int):
    return db.fetch_user(user_id)

Setting CACHEKIT_MASTER_KEY instead of passing master_key= means every preset except @cache.secure and @cache.local must state its intent (encryption=False for plaintext), or it raises ConfigurationError at decoration: the key is a key source, never an on switch.

Feature @cache.minimal @cache.dev @cache.test @cache.production @cache.secure
Default TTL 300 s 300 s 300 s 600 s 600 s
Circuit Breaker - ✅ - ✅ ✅
Backpressure ✅ ✅ - ✅ ✅
Integrity Checking - ✅ - ✅ ✅ 🔒
Encryption - - - - ✅ Required
L1 SWR (L1-only mode) - ✅ - ✅ -
Prometheus Metrics - -¹ - ✅ ✅
Structured Logging - ✅ - ✅ ✅
Backend unreachable² Function runs, no error Function runs, no error Function runs, no error Function runs, no error Function runs, no error
Use Case High throughput Local debugging Deterministic tests Production reliability Compliance/security

¹ @cache.dev exports no Prometheus metrics except circuit_breaker_state, which is recorded for every function whose circuit breaker is enabled.

² A backend that is down or unreachable never raises to the caller, on any preset (@cache.io included): the call logs the failure and runs your function. What gets cached meanwhile is in Connection Errors.

🔒 @cache.secure forces integrity_checking=True — passing integrity_checking=False raises ConfigurationError at decoration, including as an override next to config=DecoratorConfig.secure(...). Its encryption is fixed the same way: encryption= raises ConfigurationError on @cache.secure and next to config=DecoratorConfig.secure(...), so pass fail_closed= and the other encryption options to the preset itself. @cache.secure also rejects config=; the RORO form is @cache(config=DecoratorConfig.secure(...)).

Default TTL follows the cross-SDK intent-preset spec (@cache.io 3600 s) — the same numbers as cachekit-rs and cachekit-ts. ttl= overrides it; ttl=None is the explicit never-expire opt-in (details).

Cross-process L1 eviction is not a preset feature: every preset's invalidate_cache() deletes from L1 and L2 alike, and evicting other processes' L1 copies is one process-wide opt-in, CACHEKIT_INVALIDATION_LISTENER_ENABLED (L1 invalidation).

L1 SWR (within-TTL background refresh) runs only in L1-only mode (backend=None) — with a backend configured it has no effect. @cache.io additionally ships past-TTL SWR via stale_ttl (docs).

@cache.io() mirrors @cache.production (full reliability + observability) but routes to the managed CachekitIO SaaS backend instead of Redis. @cache.local() is a Python-only, SDK-local preset: a separate in-process path backed by ObjectCache (raw object references, entry-count LRU, no serialization) — the reliability and encryption features listed above do not apply to it.

Additional Presets: @cache.dev and @cache.test

@cache.dev and @cache.test are Python-only, SDK-local presets. See the comparison table above for the exact feature set of each preset.

# Development: verbose logging, integrity checks on, Prometheus off except circuit_breaker_state
@cache.dev
def debug_expensive_call():
    return complex_computation()

# Testing: deterministic, all protections off (no circuit breaker, no backpressure)
@cache.test
def test_cached_function():
    return fixed_test_value()

Architecture

┌─────────────────────────────────────────────────────────────┐
│                        Application                          │
├─────────────────────────────────────────────────────────────┤
│                     @cache Decorator                        │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │   Circuit   │  │  Adaptive   │  │    Distributed      │  │
│  │   Breaker   │  │  Timeouts   │  │      Locking        │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
├─────────────────────────────────────────────────────────────┤
│  L1 Cache (In-Memory)  │  L2 Cache (Pluggable Backend)     │
│       ~50ns            │  Redis / CachekitIO / File /      │
│                        │  Memcached    ~2-50ms             │
├─────────────────────────────────────────────────────────────┤
│                    Rust Core (PyO3)                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │    LZ4      │  │  xxHash3    │  │    AES-256-GCM      │  │
│  │ Compression │  │  Checksums  │  │    Encryption       │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

Tip

Building in Rust? The core compression, checksums, and encryption are available as a standalone crate: cachekit-core Crates.io


Features

Production Hardened

  • Circuit breaker with graceful degradation
  • Connection pooling with thread affinity (+28% throughput)
  • CachekitIO keeps idle connections pooled until the server closes them, with TCP keepalive probes, so a request after a pause skips a new TLS handshake
  • Distributed locking prevents cache stampedes
  • Pluggable backend abstraction (Redis, CachekitIO, File, Memcached, custom)
  • Untrusted-decode bounds: nesting depth and header-declared allocation are capped on every cache read (a forged entry is a bounded cache miss), verified against the protocol's shared decode-bounds.json vectors

Note

All reliability features are enabled by default with @cache.production. Use @cache.minimal to disable them for maximum throughput.

Smart Serialization

Serializer Speed Use Case
StandardSerializer ★★★★☆ General Python types; not NumPy arrays or pandas objects
OrjsonSerializer ★★★★★ JSON APIs (2-5x faster than stdlib) — requires cachekit[json]
ArrowSerializer — pandas DataFrames (columnar, exact round trip)
+ Encryption ★★★★☆ Any serializer above, AES-256-GCM encrypted: @cache.secure(master_key=..., serializer=...)
Serializer Examples
from cachekit.serializers import OrjsonSerializer, ArrowSerializer

# Fast JSON for API responses
@cache.production(serializer=OrjsonSerializer())
def get_api_response(endpoint: str):
    return {"status": "success", "data": fetch_api(endpoint)}

# Columnar DataFrames (Arrow IPC, zstd-compressed by default)
@cache(serializer=ArrowSerializer())
def get_large_dataset(date: str):
    return pd.read_csv(f"data/{date}.csv")

Encrypted DataFrames go through @cache.secure, which takes any serializer whose class declares cross_sdk_compatible = True (details). A file backend keeps this example self-contained; production uses Redis or cachekit.io:

import tempfile
from cachekit.backends.file import FileBackend, FileBackendConfig
from cachekit.serializers import ArrowSerializer

calls = 0

@cache.secure(
    master_key=secret_key,
    serializer=ArrowSerializer(),
    backend=FileBackend(FileBackendConfig(cache_dir=tempfile.mkdtemp())),
)
def get_patient_data(hospital_id: int):
    global calls
    calls += 1
    return pd.DataFrame({"hospital_id": [hospital_id], "patients": [42]})

get_patient_data(7)
get_patient_data(7)  # second call is served from the encrypted cache
assert calls == 1

Integrity Checking

Important

All serializers support configurable checksums for corruption detection using xxHash3-64 (8 bytes). Enabled by default in @cache.production and @cache.secure.

Performance Impact (benchmark-proven):

Data Type Latency Reduction (disabled) Size Overhead
MessagePack (default) 60-90% 8 bytes
Arrow DataFrames 35-49% 8 bytes
JSON (orjson) 37-68% 8 bytes

Security

Caution

When handling PII, medical, or financial data, always use @cache.secure to enforce encryption.

Key rotation: keep a retiring key readable with CACHEKIT_PREVIOUS_MASTER_KEYS (comma-separated hex, max 3 decrypt-only keys) while new writes use CACHEKIT_MASTER_KEY. A one-deploy key swap is not zero-miss; follow the key rotation runbook, including its Before You Rotate checks. CK-framed entries are selected by exact key fingerprint — never trial decryption; Interop-mode entries carry no CK frame and attempt keyring keys sequentially instead.

cachekit employs comprehensive security tooling:

  • Dependency Security: cargo-deny for license compliance + cargo-audit for RustSec scanning
  • Formal Verification: Kani proves correctness of compression, checksums, encryption
  • Runtime Analysis: Miri + sanitizers for memory safety
  • Fuzzing: Coverage-guided testing with >80% code coverage
  • Zero CVEs: Continuous vulnerability scanning
Security Commands
make security-install  # Install security tools (one-time)
make security-fast     # Run fast checks (< 3 min)

Security Tiers:

Tier Time Coverage
Fast < 3 min Vulnerability scanning, license checks, linting
Medium < 15 min Unsafe code analysis, API stability, Miri subset
Deep < 2 hours Formal verification, extended fuzzing, full sanitizers

See SECURITY.md for vulnerability reporting and detailed documentation.

Monitoring & Observability

  • Per-function statistics - cache_info() on every decorated function, modelled on functools.lru_cache
  • Prometheus metrics - Recorded by default (your app owns exposition)
  • Structured logging - Context-aware, per-operation fields
  • Health checks - Comprehensive status endpoints

Every decorated function exposes cache_info(), returning a CacheInfo named tuple with hit/miss counts, the L1/L2 split, and average backend latency:

@cache()
def get_score(x):
    return x ** 2

get_score(2)
get_score(2)  # served from cache

info = get_score.cache_info()
# CacheInfo has 9 fields: hits, misses, l1_hits, l2_hits, maxsize,
# currsize, l2_avg_latency_ms, last_operation_at, session_id
assert info.l1_hits + info.l2_hits == info.hits  # every hit is L1 or L2

maxsize and currsize are always None (the cache lives in an external store, not a bounded in-process dict); they exist only for lru_cache API parity. See the API Reference for the full field reference and a sample stats endpoint, and the Prometheus Metrics guide for metric names and exposition setup.

Thread Safety Details

Free-threaded CPython (3.14t): the core suites run green on free-threaded 3.14 with the GIL verified disabled (CI job test-freethreaded), and the Rust extension declares free-threaded safety (gil_used = false). Free-threaded wheels are not yet published and free-threaded builds are not officially supported — blocked upstream on orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import; cachekit loads redis-py only for its Redis backend, and keeps hiredis out there, so redis-py uses its pure-Python parser). See measured performance results and the full concurrency audit: docs/free-threading.md.

Per-Function Statistics:

  • Statistics tracked per function identity (module.qualname), shared across all calls and across re-decorations of the same function
  • Thread-safe via RLock (all methods safe for concurrent access)
  • Fork-safe: a forked child starts with zeroed counters and its own session ID
from concurrent.futures import ThreadPoolExecutor

@cache()
def expensive_func(x):
    return x ** 2

# All threads share same stats
with ThreadPoolExecutor(max_workers=10) as executor:
    results = list(executor.map(expensive_func, range(100)))

info = expensive_func.cache_info()
# CacheInfo(hits=..., misses=..., l1_hits=..., l2_hits=...,
#           maxsize=None, currsize=None, l2_avg_latency_ms=...,
#           last_operation_at=..., session_id=...)

Documentation

Start Here

Guide Description
Comparison Guide How cachekit compares to lru_cache, aiocache, cachetools
Getting Started Progressive tutorial from basics to advanced
API Reference Complete API documentation
Skyline (live example) Canonical example project: this SDK ingests the Bluesky firehose and writes the analytics entries a TypeScript edge Worker serves live, on one shared interop namespace

Feature Deep Dives

Feature Description
Serializer Guide ArrowSerializer vs StandardSerializer benchmarks
Circuit Breaker Prevent cascading failures
Distributed Locking Cache stampede prevention
Prometheus Metrics Built-in observability
Zero-Knowledge Encryption Client-side security
Interop Mode Cross-SDK cache sharing with cachekit-ts/rs
L1 Invalidation & SWR Invalidation scope (incl. cross-process whole-function on tenant-scoped Redis from the environment or RedisBackendProvider), opt-in cross-process L1 eviction, stale-while-revalidate
Reference Caching @cache.local() for non-serializable objects
Rust Serialization ByteStorage layer: LZ4, xxHash3, AES-256-GCM
SSRF Protection URL allowlisting for the CachekitIO backend

Configuration

Environment Variables

# Backend selection: set exactly ONE of CACHEKIT_REDIS_URL, CACHEKIT_API_KEY,
# CACHEKIT_MEMCACHED_SERVERS, CACHEKIT_FILE_CACHE_DIR. Two or more is a ConfigurationError at
# first call, and decorators relying on env auto-detection run uncached (docs/backends/README.md).
# REDIS_URL is only a fallback and never conflicts.

# Redis Connection (priority: CACHEKIT_REDIS_URL > REDIS_URL)
CACHEKIT_REDIS_URL="redis://localhost:6379"  # Primary (preferred)
REDIS_URL="redis://localhost:6379"           # Fallback

# CachekitIO SaaS Backend (closed beta — request access at cachekit.io)
# For @cache.io() next to Redis, pass api_key= from your secret store instead.
# CACHEKIT_API_KEY="your-api-key"  # pragma: allowlist secret
CACHEKIT_API_URL="https://api.cachekit.io"  # Default SaaS endpoint

# Memcached Backend (optional: pip install cachekit[memcached])
# CACHEKIT_MEMCACHED_SERVERS='["mc1:11211", "mc2:11211"]'  # Default: 127.0.0.1:11211
CACHEKIT_MEMCACHED_CONNECT_TIMEOUT=2.0                   # Default: 2.0 seconds
CACHEKIT_MEMCACHED_TIMEOUT=1.0                            # Default: 1.0 seconds
CACHEKIT_MEMCACHED_KEY_PREFIX="myapp:"                    # Default: "" (none)

# Optional Configuration
CACHEKIT_MAX_VALUE_SIZE=104857600
CACHEKIT_ARROW_COMPRESSION=zstd

Development

git clone https://github.com/cachekit-io/cachekit-py.git
cd cachekit-py
uv sync && make install
make quick-check  # format + lint + critical tests

See CONTRIBUTING.md for full development guidelines.


Requirements

Component Version
Python 3.10+

License

MIT License - see LICENSE for details.


About

Backend-agnostic caching for Python — intent-based decorators with circuit breaker, distributed locking, Prometheus metrics, and optional zero-knowledge AES-256-GCM encryption, on a Rust-powered core. Zero-config L1 in-memory; scales to Redis, Memcached, File, or CacheKit Cloud.

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages