Home › API Reference
Complete API documentation for caching with advanced reliability features.
Note
Architecture: cachekit uses a dual-layer L1+L2 caching architecture. See Data Flow Architecture for conceptual overview and Distributed Locking for multi-pod coordination.
Primary Interface: The cache decorator, configured by preset (intent) or by explicit arguments. This is the main interface for cachekit.
The @cache decorator takes its configuration from a preset or from explicit arguments.
from cachekit import cache
# Zero-config: automatic optimization (90% of use cases)
@cache(backend=None)
def expensive_function():
return do_expensive_computation()
# Intent-based optimization (9% of use cases)
# These are decorator syntax examples showing different presets:
@cache.minimal(backend=None) # Speed-critical: trading, gaming, real-time
@cache.production(backend=None) # Reliability-critical: payments, APIs
@cache.secure(master_key=secret_key) # Security-critical: PII, medical, financial (or omit master_key and set CACHEKIT_MASTER_KEY — see Encryption Parameters below)
# Manual control when needed (1% of use cases)
@cache(ttl=3600, namespace="custom", backend=None)
def custom_function():
return do_expensive_computation()Architecture: The @cache decorator resolves its configuration from the preset and arguments, then delegates to the wrapper factory for actual caching implementation.
Intent-Based Profiles. On every profile below, a backend that is down or unreachable never raises: the call logs the failure and runs your function. What gets cached meanwhile is in Connection Errors.
@cache.minimal- Speed profile: StandardSerializer (default, multi-language compatible), reduced monitoring overhead, optimized for performance@cache.production- Safety profile: StandardSerializer, all enterprise features enabled (circuit breaker, backpressure, monitoring)@cache.secure- Security profile: EncryptionWrapper, comprehensive audit logging, zero-knowledge caching@cache.dev- SDK-local, Python-only development profile: Verbose logging, easy debugging, Prometheus metrics disabled exceptcircuit_breaker_state@cache.test- SDK-local, Python-only testing profile: Deterministic behavior, all protections disabled, no monitoring for reproducible tests@cache.io- cachekit.io SaaS profile: HTTP-based edge caching via api.cachekit.io, zero infrastructure required (cachekit.io is in closed beta — request access)@cache- Baseline: no preset; every setting at its default or as passed. It never inspects the function to pick a profile, and never encrypts unless told to
When you need explicit control over caching parameters, use @cache() with manual parameter overrides. All reliability and monitoring features can be configured individually.
Tip
This decorator uses dependency injection to get the Redis client. You don't need to pass a redis_client parameter - just set the REDIS_URL or CACHEKIT_REDIS_URL environment variable.
from cachekit import cache
from cachekit.config.nested import L1CacheConfig, CircuitBreakerConfig, BackpressureConfig, MonitoringConfig
@cache(
ttl=3600,
namespace=None,
backend=None,
# Performance features
refresh_ttl_on_get=False,
ttl_refresh_threshold=0.5,
# Nested configuration groups (see sections below for details)
l1=L1CacheConfig(enabled=True),
circuit_breaker=CircuitBreakerConfig(enabled=True),
backpressure=BackpressureConfig(max_concurrent_requests=100),
monitoring=MonitoringConfig(collect_stats=True),
)
def your_function(args):
return do_expensive_computation()ttl(int | None, default:Noneon bare@cache; each preset sets its own — see the preset matrix) - Cache time-to-live in seconds.None= no expiration; on a preset that is an explicit opt-in.namespace(str | None, default:None) - Cache key prefix for organizationserializer(str | SerializerProtocol, default:"default") - Serializer name ("default","std","auto","arrow","orjson") orSerializerProtocolinstanceintegrity_checking(bool, default:True; any non-bool raisesConfigurationError) - Enable xxHash3-64 checksums for corruption detection (non-cryptographic — detects bit rot and storage bugs, NOT tampering; tamper resistance requires encryption). Behaviour when a writer and reader disagree on this flag: cross-config readskey(Callable[..., str] | None, default:None) - Custom key function for complex types; receives(*args, **kwargs)and returnsstr
refresh_ttl_on_get(bool, default:False) - Refresh TTL on async L2 hits when below threshold, in the background (no extra round trip on the hit). On CachekitIO the threshold is checked against the hit's remaining freshness; see CachekitIO TTL inspectionttl_refresh_threshold(float, default:0.5) - Minimum remaining TTL fraction (0.0–1.0) to trigger refreshl1(L1CacheConfig, default:L1CacheConfig()) - L1 in-memory cache configuration
circuit_breaker(cachekit.config.nested.CircuitBreakerConfig, default:CircuitBreakerConfig()) - Circuit breaker configuration. Not the top-levelcachekit.CircuitBreakerConfig, which configures a standaloneCircuitBreaker; see Circuit Breaker. The live values are reported byfn.get_health_status()["circuit_breaker"]["config"]:enabled(bool, default:True) - Enable circuit breaker protectionfailure_threshold(int, default:5) - Failures within a 60 s rolling window that open the circuit; successes do not reset the countsuccess_threshold(int, default:3) - Consecutive successes in half-open state before closing circuitrecovery_timeout(float, default:30.0) - Cooldown in seconds before an open circuit admits a recovery probe (reported astimeout_seconds); must be finite and> 0half_open_requests(int, default:3) - Probe slots per half-open cycle (not a concurrency limit). Every admitted probe holds one until the cycle ends, cancelled ones included, except a probe whose function raises: it gives its slot back, so more calls than this can reach the backend in one cycle. Must be>= success_threshold, or@cacheraisesConfigurationError: every success holds one of a cycle's slots, so a half-open cycle could never close
backpressure(BackpressureConfig, default:BackpressureConfig()) - Backpressure configuration:enabled(bool, default:True) - Enable backpressure protectionmax_concurrent_requests(int, default:100) - Maximum concurrent cache requestsqueue_size(int, default:1000) - Queue size for waiting requeststimeout(float, default:0.1) - Seconds to wait in queue before giving up
monitoring(MonitoringConfig, default:MonitoringConfig()) - Observability configuration:collect_stats(bool, default:True) - Collect cache hit/miss statisticsenable_structured_logging(bool, default:True) - Enable structured JSON loggingenable_prometheus_metrics(bool, default:True) - Export Prometheus metrics
encryption(EncryptionConfig | bool, default: unset) - Client-side encryption.Falsestores plaintext;True(withsingle_tenant_mode=True, orEncryptionConfig(enabled=True, single_tenant_mode=True)on a preset) encrypts. Unset (noencryption=, or anEncryptionConfigwithoutenabled=) is plaintext when no master key is present and raisesConfigurationErrorwhen one is (master_key=orCACHEKIT_MASTER_KEY), so with the variable set every plaintext cache statesencryption=False(@cache.localtakes noencryption=and never raises) (activation table).@cache.secureis the preset spelling; it sets its own encryption, soencryption=raisesConfigurationErrorthere and beside any encryptedconfig=(details)
A keyword that none of these parameters names raises ConfigurationError when the decorator is applied, on @cache, on every preset but @cache.local and beside config= (details).
master_key= (on @cache and @cache.secure) takes a hex string. A bytes key raises TypeError when the decorator is applied: pass key.hex() (details).
- Cached function result or fresh computation result
- Decorated function includes additional health check methods:
get_health_status()andcheck_health()
Modern Intelligent Interface:
from cachekit import cache
# First, set environment variable:
# export REDIS_URL="redis://localhost:6379"
@cache # Baseline: defaults, plaintext
def analyze_dataset(dataset_id, filters=None):
"""Analyze large dataset with automatic caching."""
return perform_analysis(dataset_id, filters)
@cache.production # All reliability features enabled automatically
def critical_business_function():
return important_computation()
@cache.dev # Development: verbose logging, no Prometheus except circuit_breaker_state
def debug_function():
return process_data()
@cache.test # Testing: deterministic, no protections
def test_cacheable_function():
return compute_value()Manual Configuration (fully supported):
from cachekit import cache
from cachekit.config.nested import CircuitBreakerConfig, BackpressureConfig, MonitoringConfig
@cache(ttl=1800, namespace="analytics", backend=None)
def explicit_function(dataset_id, filters=None):
return process_data(f"dataset_{dataset_id}")
# Manual configuration with nested configs for reliability features
@cache(
ttl=3600,
namespace="critical_data",
backend=None,
circuit_breaker=CircuitBreakerConfig(enabled=True),
backpressure=BackpressureConfig(enabled=True),
monitoring=MonitoringConfig(collect_stats=True, enable_structured_logging=True)
)
def critical_business_logic():
return do_expensive_computation()
# The decorator automatically connects to Redis using the environment variablecachekit.io is in closed beta — request access
Profile for zero-infrastructure caching via the cachekit.io edge network. Automatically configures CachekitIOBackend (HTTP) as the L2 backend with production-grade reliability settings (circuit breaker, L1 in-memory cache).
| Variable | Required | Default | Description |
|---|---|---|---|
CACHEKIT_API_KEY |
Yes | — | API key (ck_live_...) for authentication |
CACHEKIT_API_URL |
No | https://api.cachekit.io |
API endpoint override |
CACHEKIT_TIMEOUT |
No | 5.0 |
HTTP request timeout in seconds |
import os
from cachekit import cache
# Set your API key (or use environment variable)
# export CACHEKIT_API_KEY=ck_live_your_key_here
@cache.io(ttl=300)
def get_product_catalog(category: str):
"""Cached at the edge — no Redis needed."""
return fetch_products_from_db(category)
@cache.io(ttl=3600, namespace="reference_data")
def get_exchange_rates():
"""Multi-region edge caching with automatic L1 (in-memory) layer."""
return fetch_exchange_rates_from_api()ConfigurationError: If no API key is available (argument orCACHEKIT_API_KEY), the key is not an RFC 6750 bearer token (rules),CACHEKIT_API_URLfails validation, orbackend=/config=is passed.@cache(config=DecoratorConfig.io(...))refusesbackend=too
- Inherits all production-grade reliability features: circuit breaker, backpressure, full monitoring
- L1 in-memory cache is enabled — hot data is served at ~50ns without an HTTP round-trip
- Default
ttl=3600; with that default the stale-while-revalidate window is also 3600 s.ttl=Nonedisables both expiry and SWR. - Standard
ttl,namespace,serializer, and other@cache(...)kwargs are all supported as overrides
All functions decorated with @cache automatically include health check methods for monitoring and observability:
Note: These methods are added to the decorated function and provide comprehensive health monitoring capabilities for production deployments.
Returns current health status including circuit breaker state and backpressure metrics.
@cache(ttl=300, namespace="api")
def api_function():
return "data"
# Get health status
health_status = api_function.get_health_status()
print(health_status)
# {
# "namespace": "api",
# "features_enabled": {
# "circuit_breaker": True,
# "backpressure": True,
# "statistics": True,
# "structured_logging": True
# },
# "circuit_breaker": {
# "state": "CLOSED",
# "failure_count": 0,
# "success_count": 15,
# "last_failure_time": null,
# "next_attempt": null
# },
# "backpressure": {
# "max_concurrent": 100,
# "current_requests": 2,
# "rejected_requests": 0
# }
# }Performs an active health check and returns comprehensive status including both decorator-specific and system-wide health information.
# Perform health check
health_result = api_function.check_health() # api_function = previously decorated function
print(health_result)
# {
# "decorator": { ... }, # Same as get_health_status()
# "system": {
# "status": "healthy",
# "redis_connection": "active",
# "connection_pool": {
# "created_connections": 2,
# "available_connections": 8,
# "in_use_connections": 2
# }
# }
# }cachekit uses a modular architecture for better maintainability and testability:
# New modular structure - internal implementation detail
from cachekit.decorators.orchestrator import FeatureOrchestrator
# Orchestrator manages all reliability features (internal use)
# Users should use @cache decorator with nested configs instead
orchestrator = FeatureOrchestrator(
namespace="api_service",
circuit_breaker_enabled=True,
collect_stats=True
)Key Components:
- FeatureOrchestrator: Manages reliability features (circuit breaker, backpressure, statistics, logging)
- Thread-Local Client Caching:
get_cached_redis_client()incachekit.backends.redis.clientprovides thread-local Redis client reuse for performance - Configuration Caching: LRU cached configuration objects to eliminate overhead
Architecture Note: The implementation uses FeatureOrchestrator for better separation of concerns and improved modularity.
These components work behind the scenes to provide reliability features:
Non-blocking metrics collection system that prevents performance degradation:
- Queue-based collection with overflow protection
- Background thread processing
- Self-healing worker thread management
- Zero impact on critical path latency
Thread-local Redis client caching (cachekit.backends.redis.client):
- Eliminates repeated client creation overhead
- Thread-safe client reuse via
threading.local
The @cache decorator automatically detects and handles async functions without requiring a separate decorator.
from cachekit import cache
@cache(ttl=900)
async def fetch_user_data(user_id):
"""Fetch user data with async caching."""
async with httpx.AsyncClient() as client:
response = await client.get(f"/api/users/{user_id}")
return response.json()Tip
The @cache decorator automatically detects async functions and uses async Redis operations. No special decorator needed.
Important
cachekit uses StandardSerializer (language-agnostic MessagePack) by default to ensure cache data is compatible across Python, PHP, JavaScript, Java, and other languages.
Choose your serializer based on your use case:
Expand Decision Tree
Does your app need multi-language cache access (PHP/JS/Java/etc)?
├─ YES → Use StandardSerializer (default)
│ └─ Works with: Python, PHP, JavaScript, Java, R, Go
│ └─ Supports: None, bool, int, float, str, bytes, list, tuple, dict, datetime, date, time
│ └─ Example: @cache(ttl=3600) # No serializer parameter needed
│
└─ NO → Is your data Python-specific?
├─ NumPy arrays / pandas DataFrames / UUID / set / custom classes?
│ ├─ NumPy/UUID/set → Use AutoSerializer
│ │ └─ Example: @cache(serializer="auto")
│ │
│ └─ DataFrames?
│ └─ Use ArrowSerializer
│ └─ Example: @cache(serializer="arrow")
│
└─ JSON API responses / JSON-heavy workloads?
└─ Use OrjsonSerializer (2-5x faster than stdlib json)
└─ Example: @cache(serializer="orjson")
| Serializer | Python | PHP | JavaScript | Java/R | Go | Use Case |
|---|---|---|---|---|---|---|
| StandardSerializer (default) | ✅ | ✅ | ✅ | ✅ | ✅ | Multi-language, language-agnostic |
| AutoSerializer | ✅ | ❌ | ❌ | ❌ | ❌ | Python-only with NumPy/pandas/UUID |
| OrjsonSerializer | ✅ | ✅ | ✅ | ✅ | ✅ | JSON-native data (same as StandardSerializer) |
| ArrowSerializer | ✅ | ❌ | ✅ | ✅ | ✅ | DataFrames (NOT PHP compatible) |
Warning
- ArrowSerializer is NOT PHP-compatible - Use StandardSerializer or OrjsonSerializer for PHP
- Changing serializers re-keys the function — expect a one-time cold cache (see Serializer Switching section below)
Tip
StandardSerializer is the default - No configuration needed for multi-language compatibility.
StandardSerializer is automatically used when you don't specify a serializer:
from cachekit import cache
from datetime import datetime
# StandardSerializer is the default - language-agnostic MessagePack
@cache(ttl=3600)
def compute_results(user_id: int):
return {
"id": user_id,
"timestamp": datetime.now(),
"data": [1, 2, 3],
"nested": {"key": "value"}
}
# Cache is compatible with Python, PHP, JavaScript, Java, etc.
result = compute_results(123) # Multi-language compatibleSupported Types:
- Primitives:
None,bool,int,float,str,bytes - Collections:
list,tuple,dict - Dates:
datetime,date,time(ISO-8601 format via MessagePack extension)
Explicitly NOT supported:
- NumPy arrays → Use
serializer="auto" - pandas DataFrames/Series → Use
serializer="arrow" - UUID, set, frozenset → Use
serializer="auto" - Pydantic models, ORM models → Convert to dict first
- Custom classes → Convert to dict first
With a backend, a decorated call that returns one of these does not raise. It returns the result, caches nothing, and logs an ERROR and a WARNING on every call; see Troubleshooting → Serialization Failures. Only interop mode raises, with InteropError. Calling StandardSerializer.serialize directly raises TypeError.
Use AutoSerializer when you need Python-specific types but don't need multi-language compatibility:
from cachekit import cache
from cachekit.serializers import AutoSerializer
import numpy as np
import uuid
# AutoSerializer for Python-specific types (NumPy, UUID, set, etc.)
@cache(serializer="auto", ttl=3600)
def process_numpy_data():
return {
"array": np.array([1, 2, 3, 4, 5]),
"id": uuid.uuid4(),
"tags": {"python", "caching", "numpy"} # set support
}
# Only accessible from Python - not compatible with PHP/JS/Java
result = process_numpy_data()Use OrjsonSerializer for JSON-heavy workloads and APIs. Requires the [json] extra: pip install 'cachekit[json]' (or uv add 'cachekit[json]').
from cachekit import cache
from cachekit.serializers import OrjsonSerializer
# OrjsonSerializer for JSON APIs (2-5x faster than stdlib json)
@cache(serializer="orjson", ttl=900, backend=None)
def fetch_api_response(endpoint: str):
return {
"status": "success",
"data": fetch_external_api(endpoint) # illustrative - external API call
}
# Equivalent to StandardSerializer for language compatibility,
# but optimized for JSON serialization speed
response = fetch_api_response("/users/123")When to use OrjsonSerializer:
- JSON APIs (already producing JSON)
- Speed matters for JSON serialization
- Still want multi-language compatibility (same as StandardSerializer)
Use ArrowSerializer for DataFrames:
from cachekit import cache
from cachekit.serializers import ArrowSerializer
import pandas as pd
# ArrowSerializer for DataFrames
@cache(serializer="arrow", ttl=7200, backend=None)
def load_large_dataset(date: str):
return pd.read_csv(f"data/{date}.csv") # illustrative - file may not exist
# Returns pandas DataFrame directly
df = load_large_dataset("2024-01-01")
# Can also return as polars or pyarrow
@cache(serializer=ArrowSerializer(return_format="polars"), ttl=7200)
def load_polars_data():
import polars as pl
return pl.read_csv("data.csv")Performance: StandardSerializer does not accept DataFrames. For encoded sizes and instruction counts measured on a real 12,535-row dataset, see ArrowSerializer: Performance on Real Data.
Caution
ArrowSerializer is NOT PHP-compatible. Use StandardSerializer or OrjsonSerializer if you need PHP support.
The serializer parameter accepts:
# By name (string)
@cache(serializer="std", backend=None) # StandardSerializer (alias)
@cache(serializer="default", backend=None) # StandardSerializer
@cache(serializer="auto", backend=None) # AutoSerializer
@cache(serializer="pythonic", backend=None) # AutoSerializer (alias)
@cache(serializer="orjson", backend=None) # OrjsonSerializer
@cache(serializer="arrow", backend=None) # ArrowSerializer
# By instance (for configuration)
@cache(serializer=ArrowSerializer(return_format="polars"), backend=None)
@cache(serializer=OrjsonSerializer(option=orjson.OPT_SORT_KEYS), backend=None)
# No parameter = default StandardSerializer
@cache(ttl=3600, backend=None) # Uses StandardSerializer automaticallyThe serializer is part of the cache key, so changing it moves the function to a separate keyspace rather than colliding with its old entries:
# BEFORE: Using StandardSerializer (default) -> keys end in ":1s"
@cache
def get_data():
return df
# AFTER: Switching to ArrowSerializer -> keys end in ":1w"
@cache(serializer="arrow")
def get_data():
return df
# First call after change:
# 1. Cache miss (different key)
# 2. Function executes, caches under the new serializer's key
# 3. Subsequent calls hit normally
# 4. Old entries are never read again and expire on their TTLSee Changing Serializers for the code table, the cold-cache warning, and the data-retention caveat on orphaned entries. Upgrading from v0.19 or earlier? Keys for non-default serializers change identity without any change on your side — see Breaking change in v0.20.0.
Best Practice: Use namespace versioning for zero-downtime migrations:
# V1: StandardSerializer (existing production)
@cache(namespace="user_data:v1")
def get_user_data_v1(user_id):
return {"id": user_id, "name": "Alice"}
# V2: ArrowSerializer (new deployment, different namespace)
@cache(serializer="arrow", namespace="user_data:v2")
def get_user_data_v2(user_id):
return pd.DataFrame({"id": [user_id], "name": ["Alice"]})
# Gradual migration: switch function name in codebaseConfiguration class for backend-agnostic cache settings. Based on pydantic-settings for automatic environment variable loading with the CACHEKIT_ prefix. Redis connection settings (URL, pool size, timeouts) live on RedisBackendConfig, not here.
Key Fields:
max_value_size(int, default:104857600) - Maximum serialized value size in bytes; larger values are not cached (env:CACHEKIT_MAX_VALUE_SIZE)l1_max_size_mb(int, default:100) - Maximum L1 cache size per namespace in MB. With a backend, a single value larger than an eighth of it is not kept in L1: it is served from L2, or recomputed if L2 did not store it (env:CACHEKIT_L1_MAX_SIZE_MB)invalidation_listener_enabled(bool, default:False) - Run the cross-process L1 invalidation listener in this process: one thread and one Redis connection that evict this process's L1 copies of keys other processes invalidate; tenant-scoped Redis backend only (env:CACHEKIT_INVALIDATION_LISTENER_ENABLED). See Cross-Process L1 Evictionmaster_key(SecretStr | None, default:None) - Master encryption key for@cache.secureand explicitencryption=True(env:CACHEKIT_MASTER_KEY). A key source, not a switch — see Encryption Parameters
from cachekit.config import get_settings
# The instance the cache path reads, built from CACHEKIT_* environment variables
config = get_settings()Note: Configuration is typically loaded automatically via environment variables. The SDK reads only get_settings(), which it builds from the environment; a CachekitConfig(...) you construct yourself configures nothing. TTL is not a CachekitConfig field — see Default TTL.
The library includes intelligent error classification to distinguish between transient and permanent failures.
Note
For detailed error information, solutions, and troubleshooting, see:
- Troubleshooting Guide - Common errors and solutions
- Error Reference - Error types, causes and fixes
The tenant-scoped Redis backend (env auto-detection, RedisBackendProvider) wraps each redis-py exception in a BackendError whose error_type is a BackendErrorType; a RedisBackend you construct yourself reports every failure as UNKNOWN. The checks run in this order, and the first one an exception matches sets its category. A class can therefore land in a different category from its parent: AuthenticationError subclasses ConnectionError, and NoPermissionError, ReadOnlyError, ClusterDownError and TryAgainError subclass ResponseError.
-
AUTHENTICATION:redis.exceptions.AuthenticationError,redis.exceptions.NoPermissionError- Wrong credentials or missing ACL permission
-
TIMEOUT:redis.exceptions.TimeoutError- Operation exceeded its time limit
-
TRANSIENT:redis.exceptions.ConnectionError- Network issues; includesredis.exceptions.MaxConnectionsError(connection pool exhausted)redis.exceptions.BusyLoadingError- Redis is still loading its dataset into memoryredis.exceptions.ReadOnlyError- A read-only replica rejected a write, during failover or because writes are routed to a replicaredis.exceptions.ClusterDownError,redis.exceptions.TryAgainError- Cluster failover or resharding
-
PERMANENT:redis.exceptions.ResponseError,redis.exceptions.DataError,redis.exceptions.InvalidResponse- Client input or configuration, command, or protocol errorsredis.exceptions.LockError- Lock acquisition failures
-
UNKNOWN: any other exception
For a redis-py failure, either backend keeps only the exception's class, never the exception: the BackendError's original_exception, which is also its __cause__, is a RedisClientError (cachekit.backends.redis.error_handler) whose exc_type is that class. The exception is dropped because its traceback holds the Redis password (see SECURITY.md). Two causes are kept whole instead: an exception your own code raises inside acquire_lock or with_timeout, and a BackendError cachekit raises itself, such as for a reply of the wrong type. So check the cause's type before reading exc_type:
import redis
from cachekit.backends.redis.error_handler import RedisClientError, classify_redis_error
err = classify_redis_error(redis.exceptions.AuthenticationError("WRONGPASS"), operation="get") # what a failed get raises
cause = err.original_exception
assert isinstance(cause, RedisClientError) and issubclass(cause.exc_type, redis.exceptions.AuthenticationError)When Redis is unavailable:
- Function executes. Its result is still stored in L1, unless Redis was already down when the backend was first built: then nothing is cached (see Connection Errors)
- Warning is logged (if logging configured)
- No exception is raised to the caller
If data cannot be serialized:
- Function result is returned without caching
- Warning is logged
- No exception is raised to the caller
For error examples and handling patterns, see Troubleshooting Guide.
cachekit uses a protocol-based backend abstraction (PEP 544) that allows pluggable storage backends for L2 cache. Built-in backends include Redis, CachekitIO, File, and Memcached. You can also implement custom backends for any key-value store.
For comprehensive backend guide with examples and implementation patterns, see Backend Guide.
Resolution order, examples and the auto-detection table: Backend Resolution Priority.
For local development or when Redis is unavailable, use L1-only mode:
@cache(backend=None, l1_enabled=True)
def local_only_cache():
"""Cached in process memory only, no Redis required."""
return computation()Note: L1-only mode is process-local and not shared across pods/workers. Use for development or single-process applications only.
For complete backend implementation details, see Backend Guide - BaseBackend Protocol and Backend Guide - Custom Implementation.
cachekit is configured through environment variables. For detailed setup and troubleshooting, see Configuration Guide.
# Redis Connection (configured on RedisBackend, not CachekitConfig)
CACHEKIT_REDIS_URL=redis://localhost:6379/0
# Cache Behavior
CACHEKIT_MAX_VALUE_SIZE=104857600
CACHEKIT_ARROW_COMPRESSION=zstd
# Encryption key for @cache.secure / encryption=True (a key source, not a switch)
CACHEKIT_MASTER_KEY=<hex-encoded-key; use exactly 32 bytes>
# Fallback: REDIS_URL also supported (lower priority)
REDIS_URL=redis://localhost:6379/0
# Logging
LOG_LEVEL=INFO- CACHEKIT_REDIS_URL takes precedence over REDIS_URL
- All configuration variables must start with CACHEKIT_ (not CACHE_)
- Variables must be exported (not just set in shell)
For detailed precedence rules and troubleshooting, see Configuration Guide - Environment Variable Precedence and Configuration Guide - Troubleshooting Configuration.
cachekit includes comprehensive type hints with full basedpyright type checking (zero errors):
from __future__ import annotations
from typing import Any
from cachekit import cache
@cache(ttl=3600)
def typed_function(data: dict[str, Any]) -> str | int | None:
return process_data(data)Type Safety: The library uses from __future__ import annotations for Python 3.10+ compatibility, enabling modern union syntax (X | Y) while maintaining backward compatibility.
Every decorated function exposes cache_info(), modelled on functools.lru_cache. It
returns a CacheInfo named tuple with cumulative, per-function statistics — no Prometheus
scrape or external dependency required. CacheInfo is exported from the top-level package
(from cachekit import CacheInfo).
CacheInfo has nine fields:
| Field | Type | Meaning |
|---|---|---|
hits |
int |
Total cache hits (L1 + L2). A call that joined another call's miss on the same key in this process and got its value counts as an L1 hit (in-process single-flight) |
misses |
int |
Calls that ran the function because no cached value was found, sync or async. An async caller that waited on the distributed lock and then found the value counts as an L2 hit. A call that joined another call's miss counts as neither a hit nor a miss when that call raised |
l1_hits |
int |
L1 (in-memory) hits only |
l2_hits |
int |
L2 (backend) hits only |
maxsize |
int | None |
Always None for external caches |
currsize |
int | None |
Always None for external caches |
l2_avg_latency_ms |
float |
Average L2 (backend) latency in milliseconds |
last_operation_at |
float | None |
Unix timestamp of the last cache operation, or None |
session_id |
str | None |
Function-specific session ID for correlation |
The hit counters satisfy the invariant l1_hits + l2_hits == hits: every hit is served
either from the in-memory L1 layer or from the backend L2 layer, never both. maxsize and
currsize are kept only for lru_cache API parity and are always None because the cache
lives in an external store, not a bounded in-process dict.
Statistics are tracked per function identity (module.qualname) and are thread-safe.
Re-applying a decorator to the same function — even with different options — reuses the
existing counters from a process-global registry rather than resetting them, so session_id
stays stable and the counters reported to the CachekitIO backend stay monotonic (counters
that reset under an unchanged session ID would trip the server's anti-replay validation).
cache_clear() resets the counters and rotates session_id. A forked child process starts
with zeroed counters and a session ID derived from its own process UUID; the parent's
statistics are unaffected.
Decorate once, at module scope — as performance advice. Rebuilding the wrapper per call (e.g.
cache.io(namespace=...)(fn)inside a request handler or loop) repeats the decoration-time setup on every request and, in L1-only mode (backend=None), discards the in-process object cache each time — every call becomes a miss. Session telemetry is unaffected either way: the rebuilt wrapper reuses the same statistics tracker. If the wrapped callable must vary per call, build the decorated wrapper once, cache it (e.g. in a module-level holder orfunctools.lru_cachekeyed by namespace), and route the per-call state through an argument or a thread-local.
A minimal stats endpoint that surfaces cache_info() over HTTP:
from cachekit import cache
@cache(ttl=300)
def get_user(user_id):
return db.query(User).get(user_id)
def cache_stats():
"""Return per-function cache statistics as a JSON-serializable dict."""
info = get_user.cache_info()
total = info.hits + info.misses
return {
"hits": info.hits,
"misses": info.misses,
"l1_hits": info.l1_hits,
"l2_hits": info.l2_hits,
"hit_ratio": round(info.hits / total, 4) if total else 0.0,
"l2_avg_latency_ms": info.l2_avg_latency_ms,
"last_operation_at": info.last_operation_at,
"session_id": info.session_id,
}The library records metrics on the default prometheus_client registry (enabled by
default). Exposition is the application's responsibility — cachekit does not start an
HTTP server or register a /metrics route; wire up prometheus_client exposition
(start_http_server or make_wsgi_app/make_asgi_app) in your app. The emitted series
names carry no cachekit_ prefix:
cache_operations_total- Operation counter. Labels:operation,namespace,success,serializerredis_cache_operations_total- Load-control rejection counter. Labels:operation,status,serializer,namespacecache_operation_duration_ms- Operation latency histogram (milliseconds). Labels:operation,namespace,serializer. Buckets: 0.01, 0.05, 0.1, 0.5, 1, 2.5, 5, 10, 25, 50, 100, 250, 500, 1000,+Infcache_operation_size_bytes- Operation payload size histogram (bytes). Labels:operation,namespace,serializer. Buckets: powers of 4 from 16 to 268435456 (256 MiB),+Infcircuit_breaker_state- Gauge: number of live circuit breakers in each state. Labels:namespace,state(CLOSED,OPEN,HALF_OPEN)
The serializer label is the tier that served the record, not the @cache(serializer=...)
preset: rust = L2 backend path, l1_memory = L1 in-memory hit; unknown marks a record
emitted without the label. redis_cache_operations_total is emitted only on backpressure
rejection (operation="backpressure", status="rejected", empty serializer and namespace).
Each histogram records one observation per operation whose value is above zero, so its _count can be
lower than cache_operations_total. Releases before these bucket bounds used
prometheus_client's default buckets, sized for seconds, so the le values changed: re-check dashboards
and recording rules over the _bucket series. A p99 across all label tuples aggregates with
sum by (le):
histogram_quantile(0.99, sum by (le) (rate(cache_operation_duration_ms_bucket[5m])))
See the Prometheus Metrics guide for exposition setup, query examples, and alerting rules.
All operations include structured logging with:
- Operation context (namespace, cache key, serializer)
- Performance metrics (duration, cache hit/miss)
- Error classification and recovery actions
# Connection pooling is automatically enabled on RedisBackend
# Configure pool size via RedisBackendConfig or the backend's environment variables# Organize cache keys with namespaces
@cache(namespace="user_data")
def get_user_profile(user_id): ...
@cache(namespace="analytics")
def get_user_metrics(user_id): ...# Short TTL for frequently changing data
@cache(ttl=300) # 5 minutes
def get_live_prices(): ...
# Long TTL for stable data
@cache(ttl=86400) # 24 hours
def get_reference_data(): ...Previous: Getting Started Guide - Learn the fundamentals Next Feature Deep Dives:
- Circuit Breaker - Failure protection
- Distributed Locking - Multi-pod safety
- Serializer Guide - Choose the right serializer for your data types
- Backend Guide - Custom storage backend implementation
- Configuration Guide - Environment variable setup and tuning
- Troubleshooting Guide - Debugging and error solutions
- Error Reference - Error types, causes and fixes
| Resource | Description |
|---|---|
| Data Flow Architecture | How L1+L2 caching works |
| Performance Guide | Real benchmarks and latency characteristics |
| Prometheus Metrics | Production observability setup |