Skip to content

Latest commit

 

History

History
909 lines (678 loc) · 41 KB

File metadata and controls

909 lines (678 loc) · 41 KB

Home › API Reference

cachekit - 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.


Core Decorators

@cache - Intelligent Cache (Recommended)

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 except circuit_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

@cache(...) - Manual Configuration

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()

Core Parameters

  • ttl (int | None, default: None on 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 organization
  • serializer (str | SerializerProtocol, default: "default") - Serializer name ("default", "std", "auto", "arrow", "orjson") or SerializerProtocol instance
  • integrity_checking (bool, default: True; any non-bool raises ConfigurationError) - 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 reads
  • key (Callable[..., str] | None, default: None) - Custom key function for complex types; receives (*args, **kwargs) and returns str

Performance Parameters

  • 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 inspection
  • ttl_refresh_threshold (float, default: 0.5) - Minimum remaining TTL fraction (0.0–1.0) to trigger refresh
  • l1 (L1CacheConfig, default: L1CacheConfig()) - L1 in-memory cache configuration

Reliability Parameters

  • circuit_breaker (cachekit.config.nested.CircuitBreakerConfig, default: CircuitBreakerConfig()) - Circuit breaker configuration. Not the top-level cachekit.CircuitBreakerConfig, which configures a standalone CircuitBreaker; see Circuit Breaker. The live values are reported by fn.get_health_status()["circuit_breaker"]["config"]:
    • enabled (bool, default: True) - Enable circuit breaker protection
    • failure_threshold (int, default: 5) - Failures within a 60 s rolling window that open the circuit; successes do not reset the count
    • success_threshold (int, default: 3) - Consecutive successes in half-open state before closing circuit
    • recovery_timeout (float, default: 30.0) - Cooldown in seconds before an open circuit admits a recovery probe (reported as timeout_seconds); must be finite and > 0
    • half_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 @cache raises ConfigurationError: 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 protection
    • max_concurrent_requests (int, default: 100) - Maximum concurrent cache requests
    • queue_size (int, default: 1000) - Queue size for waiting requests
    • timeout (float, default: 0.1) - Seconds to wait in queue before giving up

Monitoring Parameters

  • monitoring (MonitoringConfig, default: MonitoringConfig()) - Observability configuration:
    • collect_stats (bool, default: True) - Collect cache hit/miss statistics
    • enable_structured_logging (bool, default: True) - Enable structured JSON logging
    • enable_prometheus_metrics (bool, default: True) - Export Prometheus metrics

Encryption Parameters

  • encryption (EncryptionConfig | bool, default: unset) - Client-side encryption. False stores plaintext; True (with single_tenant_mode=True, or EncryptionConfig(enabled=True, single_tenant_mode=True) on a preset) encrypts. Unset (no encryption=, or an EncryptionConfig without enabled=) is plaintext when no master key is present and raises ConfigurationError when one is (master_key= or CACHEKIT_MASTER_KEY), so with the variable set every plaintext cache states encryption=False (@cache.local takes no encryption= and never raises) (activation table). @cache.secure is the preset spelling; it sets its own encryption, so encryption= raises ConfigurationError there and beside any encrypted config= (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).

Returns

  • Cached function result or fresh computation result
  • Decorated function includes additional health check methods: get_health_status() and check_health()

Examples

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 variable

@cache.io — cachekit.io SaaS Backend

cachekit.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).

Environment Variables

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

Usage

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()

Raises

  • ConfigurationError: If no API key is available (argument or CACHEKIT_API_KEY), the key is not an RFC 6750 bearer token (rules), CACHEKIT_API_URL fails validation, or backend= / config= is passed. @cache(config=DecoratorConfig.io(...)) refuses backend= too

Notes

  • 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=None disables both expiry and SWR.
  • Standard ttl, namespace, serializer, and other @cache(...) kwargs are all supported as overrides

Health Check Methods

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.

get_health_status()

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
#     }
# }

check_health()

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
#         }
#     }
# }

Modular Architecture

cachekit uses a modular architecture for better maintainability and testability:

Decorator Module Structure

# 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() in cachekit.backends.redis.client provides 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.

Internal Reliability Components

These components work behind the scenes to provide reliability features:

AsyncMetricsCollector

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

get_cached_redis_client()

Thread-local Redis client caching (cachekit.backends.redis.client):

  • Eliminates repeated client creation overhead
  • Thread-safe client reuse via threading.local

Async Function Caching

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.


Serializers

Important

cachekit uses StandardSerializer (language-agnostic MessagePack) by default to ensure cache data is compatible across Python, PHP, JavaScript, Java, and other languages.

Serializer Decision Tree

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")

Language Compatibility Matrix

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.

Using StandardSerializer (Default)

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 compatible

Supported 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.

Using AutoSerializer (Python-Only)

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()

Using OrjsonSerializer (JSON-Optimized)

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)

Using ArrowSerializer (DataFrame-Optimized)

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.

Serializer Parameter Format

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 automatically

Serializer Switching

The 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 TTL

See 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 codebase

Configuration Classes

CachekitConfig

Configuration 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 Eviction
  • master_key (SecretStr | None, default: None) - Master encryption key for @cache.secure and explicit encryption=True (env: CACHEKIT_MASTER_KEY). A key source, not a switch — see Encryption Parameters

Example

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.


Error Handling and Classification

The library includes intelligent error classification to distinguish between transient and permanent failures.

Note

For detailed error information, solutions, and troubleshooting, see:

Error Categories

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.

  1. AUTHENTICATION:

    • redis.exceptions.AuthenticationError, redis.exceptions.NoPermissionError - Wrong credentials or missing ACL permission
  2. TIMEOUT:

    • redis.exceptions.TimeoutError - Operation exceeded its time limit
  3. TRANSIENT:

    • redis.exceptions.ConnectionError - Network issues; includes redis.exceptions.MaxConnectionsError (connection pool exhausted)
    • redis.exceptions.BusyLoadingError - Redis is still loading its dataset into memory
    • redis.exceptions.ReadOnlyError - A read-only replica rejected a write, during failover or because writes are routed to a replica
    • redis.exceptions.ClusterDownError, redis.exceptions.TryAgainError - Cluster failover or resharding
  4. PERMANENT:

    • redis.exceptions.ResponseError, redis.exceptions.DataError, redis.exceptions.InvalidResponse - Client input or configuration, command, or protocol errors
    • redis.exceptions.LockError - Lock acquisition failures
  5. 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)

Connection Failures

When Redis is unavailable:

  1. 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)
  2. Warning is logged (if logging configured)
  3. No exception is raised to the caller

Serialization Failures

If data cannot be serialized:

  1. Function result is returned without caching
  2. Warning is logged
  3. No exception is raised to the caller

For error examples and handling patterns, see Troubleshooting Guide.

Backend Abstraction

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.

Backend Resolution Priority

Resolution order, examples and the auto-detection table: Backend Resolution Priority.

L1-Only Mode (No Backend)

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.

Environment Variables

cachekit is configured through environment variables. For detailed setup and troubleshooting, see Configuration Guide.

Standard Configuration

# 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

Variable Precedence

  • 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.

Type Hints

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.

Monitoring and Observability

Per-function statistics via cache_info()

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 or functools.lru_cache keyed 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,
    }

Prometheus Metrics

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, serializer
  • redis_cache_operations_total - Load-control rejection counter. Labels: operation, status, serializer, namespace
  • cache_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, +Inf
  • cache_operation_size_bytes - Operation payload size histogram (bytes). Labels: operation, namespace, serializer. Buckets: powers of 4 from 16 to 268435456 (256 MiB), +Inf
  • circuit_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.

Structured Logging

All operations include structured logging with:

  • Operation context (namespace, cache key, serializer)
  • Performance metrics (duration, cache hit/miss)
  • Error classification and recovery actions

Best Practices

Connection Management

# Connection pooling is automatically enabled on RedisBackend
# Configure pool size via RedisBackendConfig or the backend's environment variables

Namespace Organization

# Organize cache keys with namespaces
@cache(namespace="user_data")
def get_user_profile(user_id): ...

@cache(namespace="analytics")
def get_user_metrics(user_id): ...

TTL Strategy

# 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(): ...

Next Steps

Previous: Getting Started Guide - Learn the fundamentals Next Feature Deep Dives:

See Also

Related Guides

Architecture & Performance

Resource Description
Data Flow Architecture How L1+L2 caching works
Performance Guide Real benchmarks and latency characteristics
Prometheus Metrics Production observability setup