Home › Configuration Guide
Configure cachekit through environment variables and decorator parameters
Note
This is the canonical reference for all cachekit environment variables.
cachekit supports four backends. Pick the one that fits your infrastructure:
| Backend | Best For | Required Config |
|---|---|---|
| Redis | Self-hosted, full control | CACHEKIT_REDIS_URL |
| CachekitIO | Managed, zero-ops | CACHEKIT_API_KEY |
| File | Local dev, testing | None |
| Memcached | High-throughput, existing infra | CACHEKIT_MEMCACHED_SERVERS |
By default @cache auto-detects your backend from env vars, falling back to Redis at localhost (12-factor convention). CachekitIO is a managed alternative (currently in closed beta). File backend is intended for local development and testing only. Memcached is an optional backend (pip install cachekit[memcached]).
For self-hosted Redis infrastructure:
Usage:
from cachekit import cache
@cache # Auto-detects backend (defaults to Redis at localhost)
def fetch_data():
return expensive_computation()Configuration: See Redis Environment Variables below.
cachekit.io is in closed beta — request access
Zero-ops managed caching via the cachekit.io SaaS API. No Redis to provision or maintain.
Configuration: See CachekitIO Configuration below.
File-based cache for local development and testing. No external services required.
Configuration: See the File Backend section below.
Requires:
pip install cachekit[memcached]
High-throughput in-memory caching with consistent hashing across multiple servers.
Configuration: See the Memcached Backend section below.
Configure Redis backend through environment variables:
# Redis Connection
CACHEKIT_REDIS_URL=redis://localhost:6379/0
CACHEKIT_CONNECTION_POOL_SIZE=10
CACHEKIT_SOCKET_TIMEOUT=1.0
CACHEKIT_SOCKET_CONNECT_TIMEOUT=1.0
# Cache Behavior
CACHEKIT_DEFAULT_TTL=3600
CACHEKIT_MAX_VALUE_SIZE=104857600
CACHEKIT_ARROW_COMPRESSION=zstd
# Encryption (for @cache.secure)
CACHEKIT_MASTER_KEY=<hex-encoded-key-32-bytes-minimum>
# Key rotation: decrypt-only previous master keys (comma-separated hex, max 3,
# same per-key requirements as CACHEKIT_MASTER_KEY). Entries written under a
# listed key stay readable through the rotation window; writes always use
# CACHEKIT_MASTER_KEY. More than 3 keys, or the current master key re-appearing
# in this list, is rejected at load.
CACHEKIT_PREVIOUS_MASTER_KEYS=<old-key-hex>,<older-key-hex>
# Fail closed on decrypt authentication failures (default: false = fail open/recompute).
# When true, AES-GCM auth failures and key-fingerprint mismatches raise
# DecryptionAuthenticationError to the caller instead of silently recomputing.
# Per-decorator fail_closed=True/False overrides this fleet-wide default.
CACHEKIT_ENCRYPTION_FAIL_CLOSED=false
# Fallback: REDIS_URL also supported (lower priority)
REDIS_URL=redis://localhost:6379/0
# Logging
LOG_LEVEL=INFO
# Performance Testing
REQUESTS_CA_BUNDLE= # Unset to avoid SSL issuescachekit.io is in closed beta — request access
Configure the CachekitIO managed backend through environment variables. All fields use the CACHEKIT_ prefix.
# Required: API key for authentication (ck_live_... format)
CACHEKIT_API_KEY=ck_live_your_key_here
# Optional: API endpoint (default: https://api.cachekit.io)
CACHEKIT_API_URL=https://api.cachekit.io
# Optional: Request timeout in seconds (default: 5.0, must be > 0)
CACHEKIT_TIMEOUT=5.0
# Optional: Maximum retry attempts for transient errors (default: 3, minimum: 0)
CACHEKIT_MAX_RETRIES=3
# Optional: HTTP connection pool size (default: 10, must be > 0)
CACHEKIT_CONNECTION_POOL_SIZE=10
# Optional: Allow custom API hostname - disables SSRF hostname allowlist (default: false)
# Only set to true when pointing at a private test server
CACHEKIT_ALLOW_CUSTOM_HOST=false| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
CACHEKIT_API_KEY |
SecretStr |
— | Yes | API key (ck_live_...) for authentication |
CACHEKIT_API_URL |
str |
https://api.cachekit.io |
No | API endpoint URL (must use HTTPS) |
CACHEKIT_TIMEOUT |
float |
5.0 |
No | Per-request timeout in seconds |
CACHEKIT_MAX_RETRIES |
int |
3 |
No | Max retry attempts for transient errors |
CACHEKIT_CONNECTION_POOL_SIZE |
int |
10 |
No | Max HTTP connections in pool |
CACHEKIT_ALLOW_CUSTOM_HOST |
bool |
false |
No | Disable hostname allowlist (testing only) |
Security notes:
CACHEKIT_API_URLmust use HTTPS. HTTP is rejected at startup.- Private/internal IP addresses are blocked (SSRF protection). This includes
10.x,172.16-31.x,192.168.x,127.x, and link-local ranges. CACHEKIT_ALLOW_CUSTOM_HOST=truedisables the hostname allowlist. Only use with trusted configuration (e.g., a local test server running over HTTPS with a self-signed cert).
import os
from cachekit import cache
os.environ["CACHEKIT_API_KEY"] = "ck_live_your_key_here"
# All production-grade features enabled: L1, circuit breaker, monitoring
@cache.io(ttl=300)
def fetch_data(user_id: int):
return expensive_api_call(user_id)@cache.io() automatically creates a CachekitIOBackend from environment variables. It applies production-grade defaults (see the intent preset table below).
@cache.io supports past-TTL stale-while-revalidate (protocol spec): for a stale_ttl-second window after the fresh TTL lapses, the backend keeps serving the old value (flagged stale) and the SDK re-runs your function in the background — no request ever blocks on the recompute at a TTL boundary.
from cachekit import cache
# Default: @cache.io enables SWR with stale_ttl = ttl.
@cache.io(ttl=300)
def build_index():
return expensive_scan()
# Size the window explicitly, or pass stale_ttl=0 to opt out.
@cache.io(ttl=300, stale_ttl=900)
def report():
return expensive_report()Rules and behavior:
- Requires a positive
ttl;ttl + stale_ttlis capped at 2,592,000 s (30 days). Violations raiseConfigurationErrorat decoration time. - CachekitIO only — other backends have no read-side freshness signal and raise
ConfigurationErrorifstale_ttlis set. - Concurrent stale hits trigger at most one revalidation: per-process dedup plus (async functions) a non-blocking distributed lease on the backend's lock. Contested = serve stale, don't wait.
- A failed background recompute is silent: the entry keeps serving stale until its hard eviction bound, after which the next call takes the ordinary synchronous miss path.
- The background recompute runs with a snapshot of the caller's
contextvars(contextvar-based tenant extraction works), but outside the request otherwise — don't rely on other request-scoped resources (open sessions, connections) inside functions that enable SWR. - Stale values are never written to the L1 in-memory cache, and stale reads still count as cache hits for metered-misses billing.
# Directory for cache files (default: system temp dir + /cachekit)
CACHEKIT_FILE_CACHE_DIR=/tmp/cachekit
# Maximum total cache size in MB (default: 1024, range: 1-1,000,000)
CACHEKIT_FILE_MAX_SIZE_MB=1024
# Maximum single value size in MB (default: 100, max: 50% of MAX_SIZE_MB)
CACHEKIT_FILE_MAX_VALUE_MB=100
# Maximum number of cache entries (default: 10000, range: 100-1,000,000)
CACHEKIT_FILE_MAX_ENTRY_COUNT=10000
# Lock acquisition timeout in seconds (default: 5.0, range: 0.5-30.0)
CACHEKIT_FILE_LOCK_TIMEOUT_SECONDS=5.0Requires:
pip install cachekit[memcached]oruv add cachekit[memcached]
# Server list (JSON array format, default: ["127.0.0.1:11211"])
CACHEKIT_MEMCACHED_SERVERS='["mc1:11211", "mc2:11211"]'
# Timeouts
CACHEKIT_MEMCACHED_CONNECT_TIMEOUT=2.0 # Default: 2.0 seconds (range: 0.1-30.0)
CACHEKIT_MEMCACHED_TIMEOUT=1.0 # Default: 1.0 seconds (range: 0.1-30.0)
# Connection pool
CACHEKIT_MEMCACHED_MAX_POOL_SIZE=10 # Default: 10 per server (range: 1-100)
CACHEKIT_MEMCACHED_RETRY_ATTEMPTS=2 # Default: 2 (range: 0-10)
# Optional key prefix for namespace isolation
CACHEKIT_MEMCACHED_KEY_PREFIX="myapp:" # Default: "" (none)Configure L1 (in-memory) cache behavior through L1CacheConfig:
from cachekit import cache
from cachekit.config import L1CacheConfig
# Enable SWR (stale-while-revalidate) with custom threshold.
# SWR requires a ttl — with ttl=None entries never go stale, so no refresh fires.
@cache(
ttl=3600,
l1=L1CacheConfig( # Correct parameter name is 'l1', not 'l1_config'
enabled=True,
max_size_mb=100,
swr_enabled=True,
swr_threshold_ratio=0.5, # Refresh at 50% of TTL
),
backend=None
)
def my_function():
return expensive_computation() # illustrative - not defined| Field | Type | Default | Purpose |
|---|---|---|---|
enabled |
bool | True |
Enable L1 in-memory cache |
max_size_mb |
int | 100 |
Maximum L1 cache size in MB |
swr_enabled |
bool | True |
Enable stale-while-revalidate (SWR) — L1-only mode only |
swr_threshold_ratio |
float | 0.5 |
Refresh at X% of TTL, in (0.0, 1.0] — L1-only mode only |
L1 Cache Concepts:
- Freshness: When to serve stale data + trigger background refresh (SWR, L1-only mode only — with a backend configured these fields have no effect)
- Expiry: Hard deadline when entry is deleted from cache
With backend=None the decorator caches raw Python objects in process memory (no
serialization — tuples, sets, and frozensets keep their types). L1CacheConfig is
honored as follows:
max_size_mbbounds the cache by estimated bytes, not entry count. Sizes of raw objects are estimated best-effort (builtin containers are walked recursively; other objects are counted viasys.getsizeof). A single value larger than the whole budget is returned to the caller but never cached.- SWR requires a
ttl. Withswr_enabled=Trueand attlset, a cache hit pastttl * swr_threshold_ratio(±10% jitter) serves the cached value immediately and refreshes it in the background — viaasyncio.create_taskforasync deffunctions, or a daemon thread for sync functions. A successful refresh restarts both the freshness clock and the TTL. Withttl=Noneentries never go stale, so no refresh is ever scheduled — they are stored with a one-year (31,536,000 s) sentinel expiry rather than truly indefinitely, and can still be evicted earlier under byte pressure. - Refresh failures are non-fatal: the stale value keeps being served until hard expiry, and the next qualifying hit retries the refresh.
import asyncio
from cachekit import cache
from cachekit.config import L1CacheConfig
@cache(ttl=60, backend=None, l1=L1CacheConfig(swr_enabled=True, swr_threshold_ratio=0.5))
async def load_dashboard():
return await fetch_expensive_data() # illustrative - not defined
# After ~30s (50% of TTL, ±10% jitter), the next call returns the cached value
# instantly and schedules a background refresh — callers never block on revalidation.Use intent presets to configure L1 and other features for different use cases:
from cachekit import cache
# Zero overhead - all features disabled
@cache.minimal(backend=None)
def minimal_function():
pass
# Development - SWR enabled, invalidation disabled
@cache.dev(backend=None)
def dev_function():
pass
# Production - all features enabled
@cache.production(backend=None)
def prod_function():
pass
# Testing - deterministic behavior
@cache.test(backend=None)
def test_function():
pass
# Secure - encryption + all features
@cache.secure(master_key="a" * 64, backend=None)
def secure_function():
pass
# cachekit.io SaaS - production-grade via managed API (closed beta)
# Requires: CACHEKIT_API_KEY env var
# @cache.io(ttl=300)
# def io_function():
# passFeature Matrix by Intent:
| Intent | SWR | Invalidation | Max Size | Notes |
|---|---|---|---|---|
minimal() |
❌ | ❌ | 100 MB | Speed-first, no integrity check |
test() |
❌ | ❌ | 100 MB | Deterministic, no monitoring |
dev() |
L1-only¹ | ❌ | 100 MB | Verbose logs, no Prometheus |
production() |
L1-only¹ | ✓ | 100 MB | Full observability |
secure() |
L1-only¹ | ✓ | 100 MB | AES-256-GCM encryption required |
io() |
✓ | ✓ | 100 MB | CachekitIO managed SaaS backend (closed beta — request access); past-TTL SWR default-on (stale_ttl = ttl) |
¹ Within-TTL refresh-ahead SWR runs only in L1-only mode (backend=None), where the SDK re-runs your function in the background past ttl * swr_threshold_ratio. With a backend configured, these presets have no SWR — swr_enabled has no effect outside L1-only mode (Redis exposes no read-side freshness signal). The only backed SWR is @cache.io's past-TTL stale_ttl mode.
Important
When multiple environment variables could apply, cachekit follows this priority order.
| Priority | Variable | Description |
|---|---|---|
| 1 | CACHEKIT_REDIS_URL |
Explicit cachekit-specific connection |
| 2 | REDIS_URL |
Fallback (only if CACHEKIT_REDIS_URL not set) |
Example:
# This configuration uses CACHEKIT_REDIS_URL (REDIS_URL is ignored)
export CACHEKIT_REDIS_URL=redis://prod.example.com:6379/0
export REDIS_URL=redis://localhost:6379/0 # Ignored - won't be used@cache.io() reads from CachekitIOBackendConfig — a completely separate config class from CachekitConfig. Redis URL precedence does not apply.
| Decorator | Config Class | Key Variable |
|---|---|---|
@cache, @cache.production(), etc. |
CachekitConfig |
CACHEKIT_REDIS_URL / REDIS_URL |
@cache.io() |
CachekitIOBackendConfig |
CACHEKIT_API_KEY |
Setting REDIS_URL has no effect on @cache.io(), and setting CACHEKIT_API_KEY has no effect on Redis-backed decorators.
For local development without Redis:
# Disable Redis backend, use L1 only
# Set decorator with backend=None@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 production with Redis:
export CACHEKIT_REDIS_URL=redis://redis-primary:6379/0
export CACHEKIT_CONNECTION_POOL_SIZE=20
export CACHEKIT_DEFAULT_TTL=3600
export CACHEKIT_ARROW_COMPRESSION=zstdFor production with sensitive data:
export CACHEKIT_REDIS_URL=redis://redis-primary:6379/0
export CACHEKIT_MASTER_KEY=$(openssl rand -hex 32)
export CACHEKIT_ARROW_COMPRESSION=zstd
export LOG_LEVEL=WARNINGIssue: "Redis connection error"
Causes:
- Redis service not running
- Wrong Redis URL format
- Firewall blocking connection
- Authentication failed
Solutions:
# Check Redis is running
redis-cli ping
# Output: PONG
# Verify connection string format
export CACHEKIT_REDIS_URL=redis://localhost:6379/0
# NOT: redis:localhost:6379 (wrong)
# NOT: redis/localhost:6379 (wrong)
# Test connection from Python
python -c "
import redis
r = redis.from_url('redis://localhost:6379/0')
print(r.ping()) # Should print True
"Issue: Both CACHEKIT_REDIS_URL and REDIS_URL set - which is used?
Priority order: CACHEKIT_REDIS_URL > REDIS_URL
If you have both set and want to verify which is being used:
import os
from cachekit import cache
# Check which URL was loaded
print(f"CACHEKIT_REDIS_URL: {os.getenv('CACHEKIT_REDIS_URL')}")
print(f"REDIS_URL: {os.getenv('REDIS_URL')}")
# CACHEKIT_REDIS_URL takes precedence if both are set
@cache()
def test_function():
return "cached"Issue: Wrong prefix used (CACHE_ instead of CACHEKIT_)
[!CAUTION] All cachekit environment variables start with CACHEKIT_ (not CACHE_).
# WRONG - won't be read:
export CACHE_REDIS_URL=redis://localhost:6379
# CORRECT - will be read:
export CACHEKIT_REDIS_URL=redis://localhost:6379Issue: Variable not being read
Check if variable is properly exported:
# Print all CACHEKIT variables
env | grep CACHEKIT
# Ensure variable is exported (not just set in shell)
export CACHEKIT_REDIS_URL=redis://localhost:6379 # Correct - variable is exported
CACHEKIT_REDIS_URL=redis://localhost:6379 # Wrong - not exported to child processesIssue: "CACHEKIT_MASTER_KEY not set" when using encryption
When using @cache.secure(), the master key is required:
# Generate secure master key
export CACHEKIT_MASTER_KEY=$(openssl rand -hex 32)
# Verify key is set
python -c "import os; print(f'Key length: {len(os.getenv(\"CACHEKIT_MASTER_KEY\", \"\"))}')"
# Output: Key length: 64 (hex-encoded 32 bytes)Valid key formats:
- 64 character hex string (32 bytes)
- Minimum requirement: 32 bytes (64 hex characters)
Invalid key formats:
# WRONG - not hex
export CACHEKIT_MASTER_KEY="my-secret-key"
# WRONG - too short
export CACHEKIT_MASTER_KEY="abcd1234"
# CORRECT - 64 hex characters = 32 bytes
export CACHEKIT_MASTER_KEY="a1b2c3d4e5f6789012345678901234567890abcdef12345678901234567890"Issue: Connection timeout errors
Adjust timeout settings:
# Increase timeout values (in seconds)
export CACHEKIT_SOCKET_TIMEOUT=5.0
export CACHEKIT_SOCKET_CONNECT_TIMEOUT=5.0
# These are often needed for:
# - High-latency networks
# - Slow Redis instance
# - High load scenarios# Reject values whose serialized envelope exceeds this many bytes (L2 ceiling)
export CACHEKIT_MAX_VALUE_SIZE=104857600 # Default is 100MB
# Tighten for memory-constrained environments
export CACHEKIT_MAX_VALUE_SIZE=10485760 # 10MBArrow payloads are compressed automatically. Select the codec:
# zstd (default), lz4, or none
export CACHEKIT_ARROW_COMPRESSION=zstdTip
Compression saves network bandwidth for large values. none can enable zero-copy mmap reads on the File backend — eligibility also requires a plaintext (unencrypted) Arrow payload read back as pandas (return_format="pandas") and a backend that supports buffer reads. Writes are independent of this setting: plaintext Arrow values stream to the File backend record batch by record batch (compressed or not), never materializing the full payload in memory; encrypted values and other backends use the buffered write path.
# Tune connection pool size based on concurrency
export CACHEKIT_CONNECTION_POOL_SIZE=20 # Default is 10
# Higher for:
# - Many concurrent requests
# - High-concurrency workloads
#
# Lower for:
# - Memory-constrained environments
# - Single-threaded applications- API Reference - All decorator parameters
- Troubleshooting Guide - Common errors and solutions
- Zero-Knowledge Encryption - Encryption setup
- Getting Started - Quick start guide