Configuration
Basic setup
CACHES = {
"default": {
"BACKEND": "django_vcache.backend.ValkeyCache",
"LOCATION": "valkey://your-valkey-host:6379/1",
},
}
URL schemes
| Scheme | Description |
|---|---|
valkey:// |
Standard Valkey connection |
redis:// |
Standard Redis connection |
valkeys:// |
TLS-encrypted Valkey connection |
rediss:// |
TLS-encrypted Redis connection |
sentinel:// |
Valkey/Redis Sentinel |
Options
Options are set in the OPTIONS dictionary:
CACHES = {
"default": {
"BACKEND": "django_vcache.backend.ValkeyCache",
"LOCATION": "valkey://your-valkey-host:6379/1",
"OPTIONS": {
"SERIALIZER": "msgpack",
"COMPRESS_MIN_LEN": 1024,
},
},
}
SERIALIZER
Default: "msgpack"
Choose the serialization format:
"msgpack"(default) — Fast and secure. Uses ormsgpack. Cannot execute arbitrary code on deserialization."pickle"— For projects that need to cache arbitrary Python objects (Django models, custom classes, etc.).
Warning
Pickle can execute arbitrary code on deserialization. Only use it if you trust all data in your cache.
COMPRESS_MIN_LEN
Default: 1024
Values larger than this threshold (in bytes) are automatically compressed with zstd. Set to 0 to disable compression.
"OPTIONS": {
"COMPRESS_MIN_LEN": 2048, # compress values larger than 2KB
}
IGNORE_EXCEPTIONS
Default: False
When True, connection failures degrade to cache misses instead of raising:
get returns the default, set/delete/touch return False, incr
returns 0, get_many returns {}, and set_many reports every key as
failed. Use this when a down cache server should degrade gracefully rather
than take requests down with it.
"OPTIONS": {
"IGNORE_EXCEPTIONS": True,
}
Only connection-level errors are swallowed — data errors (e.g. incr on a
non-integer value) always raise.
Note
With the default IGNORE_EXCEPTIONS: False, the resilience wrapper is
stripped from the hot path at init time by rebinding the undecorated
methods onto the cache instance. A side effect: patching methods on the
ValkeyCache class (e.g. mock.patch.object(ValkeyCache, "get"))
does not affect already-created instances. Patch the instance instead
(mock.patch.object(cache, "get")), which works and restores correctly.
CLIENT_SIDE_CACHE
Default: disabled
Opt-in server-assisted client-side caching
(requires Valkey/Redis 6.0+ and RESP3). Repeated gets of hot keys are
served from an in-process cache that the server invalidates on writes:
"OPTIONS": {
"CLIENT_SIDE_CACHE": {
"ENABLED": True,
"MAX_SIZE": 10_000, # cached entries (default)
"TTL": 300, # seconds (default)
},
}
cache.get_raw_client().cache_statistics() returns
(hits, misses, invalidations) or None when disabled. Not supported in
cluster mode.
DRIVER_CLASS
Default: the bundled RustValkeyDriver
Advanced hook for downstream Rust extensions: accepts a driver class or a
dotted-path string implementing ValkeyDriverProtocol
(see django_vcache/backend.py). Lets an application that ships its own
Rust extension route the cache through a driver living in that extension's
.so, so the cache shares one tokio runtime and connection with the app's
other Rust I/O instead of running two runtimes side by side.
"OPTIONS": {
"DRIVER_CLASS": "myapp._rust.ValkeyDriver",
}
Pick one driver source per process — the bundled driver and a downstream
.so each carry their own tokio runtime, so mixing them runs two runtimes.
CLUSTER_MODE
Default: False
Enable for Valkey/Redis Cluster deployments. The LOCATION should point to one of the cluster's nodes; the driver automatically discovers the rest.
CACHES = {
"default": {
"BACKEND": "django_vcache.backend.ValkeyCache",
"LOCATION": "valkey://your-cluster-node-1:6379/0",
"OPTIONS": {
"CLUSTER_MODE": True,
},
},
}
Note
Distributed locking (cache.lock() and cache.alock()) is not supported in cluster mode. Attempting to use these methods will raise NotImplementedError.
Sentinel
Use a sentinel:// URL to connect via Valkey/Redis Sentinel. The URL format is:
sentinel://sentinel-host:26379/master-name/db
Example:
CACHES = {
"default": {
"BACKEND": "django_vcache.backend.ValkeyCache",
"LOCATION": "sentinel://sentinel-host:26379/mymaster/1",
},
}
The driver automatically re-discovers the master on failover.
Authentication
Sentinel returns only the master's host and port on discovery — never its credentials — so a password-protected master must be authenticated separately. The URL userinfo carries the master's credentials:
sentinel://:master-password@sentinel-host:26379/master-name/db
sentinel://master-user:master-password@sentinel-host:26379/master-name/db
This covers the common topology of unauthenticated Sentinels in front of a
password-protected master. When the Sentinel nodes themselves require
authentication, supply their credentials separately with the
sentinel_username / sentinel_password query parameters — these are distinct
from the master credentials:
sentinel://:master-password@sentinel-host:26379/master-name/db?sentinel_password=sentinel-password
Credentials in the URL must be percent-encoded
if they contain reserved characters (@, :, /, etc.).
IPv6 Sentinel hosts must be bracketed:
sentinel://[2001:db8::1]:26379,[2001:db8::2]:26379/master-name/db
TLS certificates
Use a valkeys:// or rediss:// URL scheme with ssl_ca_certs to connect over TLS:
CACHES = {
"default": {
"BACKEND": "django_vcache.backend.ValkeyCache",
"LOCATION": "valkeys://your-valkey-host:6380/1",
"OPTIONS": {
"ssl_ca_certs": "/path/to/ca.crt",
"ssl_certfile": "/path/to/client.crt", # optional, for mTLS
"ssl_keyfile": "/path/to/client.key", # optional, for mTLS
},
},
}
ssl_ca_certs
Path to a PEM file containing the CA certificate used to verify the server's certificate. This is required for TLS connections — mount your CA certificate into the container and reference it here.
ssl_certfile / ssl_keyfile
Paths to PEM files for mutual TLS (mTLS) client authentication. Both must be provided together. Only needed when the server requires client certificate authentication.
ssl_cert_reqs
Default: "required"
Set to "none" to skip certificate verification entirely (uses TLS encryption without validating the server's certificate). This is useful for self-signed certificates when you don't have access to the CA cert:
"OPTIONS": {
"ssl_cert_reqs": "none",
}
Rotating credentials
Some deployments authenticate with a credential that expires: an AWS
ElastiCache IAM auth token lives 15 minutes, Vault's dynamic secrets carry a
lease, Azure Entra ID and GCP IAM issue similar short-lived tokens. A password
read once from LOCATION cannot serve those. Valkey checks the password only
at AUTH/HELLO, so a connection already open keeps working — but the driver
opens new ones on its own schedule (reconnect after a restart or failover, an
idle socket reaped by a load balancer, the blocking-connection pool growing for
django-vtasks), and those fail once the credential is stale. ElastiCache goes
further and disconnects an IAM-authenticated connection after 12 hours
unless a fresh AUTH has been sent on it.
credential_provider replaces the URL password with a callable the driver
consults, and keeps the result current for as long as the driver lives.
| Key | Default | Meaning |
|---|---|---|
credential_provider |
— | Dotted path to (or a reference to) the callable. Supersedes any password in LOCATION. |
credential_provider_options |
{} |
Opaque mapping handed to the provider as conn["options"]. Provider-specific configuration goes here. |
credential_ttl |
600 |
Refresh interval used only when a provider reports no expiry. |
credential_provider_allow_plaintext |
False |
Permit a non-TLS LOCATION. Local development only. |
Writing a provider
A provider is a plain callable. It receives the connection being opened and returns the secret:
def my_token(conn) -> str:
return mint_for(conn["host"], conn["user"], conn["options"]["tenant"])
CACHES = {
"default": {
"BACKEND": "django_vcache.backend.ValkeyCache",
"LOCATION": "valkeys://app-user@cache.example.com:6379/0",
"OPTIONS": {
"credential_provider": "myapp.cache.my_token",
"credential_provider_options": {"tenant": "prod"},
},
}
}
conn carries host, port, user (the URL userinfo, or None for the
default user), db, and options — a copy of credential_provider_options.
The options mapping exists because a credential is often scoped to something
the hostname does not reveal (an ElastiCache token signs the cache name), and
because one importable function should be able to serve several cache aliases
with different issuer parameters.
Or return a mapping, which is what real issuers hand back:
| Returned | Meaning |
|---|---|
"secret" |
The password. Nothing else rotates. |
{"password": ...} |
The same, spelled out. |
{"password": ..., "expires_at": ...} |
Renewal follows the issuer's real deadline. expires_at is a datetime or a POSIX timestamp. |
{"password": ..., "username": ...} |
The username rotates too — the connection re-AUTHs as the new ACL user. |
Reporting expires_at means the driver never has to guess: it renews five
minutes ahead of the stated deadline, but never before the token's half-life
(so a five-minute lease is renewed at two and a half minutes, not every few
seconds), and credential_ttl is only the fallback for issuers that say
nothing.
Unrecognised keys are rejected rather than ignored, so a user-for-username
typo fails loudly instead of silently authenticating as the configured user.
This is the same contract as django-vpg's credential_provider, down to the
user and options keys, so a provider written for the database can usually
be adapted for the cache by changing what it signs (db here is the Valkey
database index; vpg passes dbname).
How it behaves
- Refresh is the driver's job, not a command's. A dedicated thread keeps the credential current, so no cache op ever pays a provider round trip, and a driver used from Rust through an embedded build (which never calls back into Python) stays authenticated just the same.
- Every connection re-authenticates on refresh. Each connection — the
multiplexed one for fast ops, each blocking-pool connection, every cluster
node, the Sentinel-discovered master — subscribes to the credential stream
and sends
AUTHwhen a new one arrives. A reconnect takes whatever is current. This is what keeps a long-lived ElastiCache connection alive past the 12-hour limit without ever being torn down. One caveat from redis-rs: if a re-AUTHon a connection ever fails, that connection stops being re-authenticated and ElastiCache will disconnect it at the 12-hour mark; the reconnect then takes the current token and the transport retry absorbs the blip, so the cost is one reconnect rather than a lost credential. - A rejected credential is a signal. When the server refuses the
credential on a connect (
WRONGPASS), the driver asks the provider once and retries that command. A rotating ACL password moved by a job the driver cannot see is absorbed without an error reaching the application. Repeated rejections back off exponentially, so a user missing from the cache's user group settles into a slow retry rather than a mint per failed command. - A failed refresh does not fail commands. The existing credential keeps
being served and the error is recorded — readable as
cache.get_raw_client().credential_errorfor a health check. You only get errors once the server actually stops accepting it. - The provider must be importable by name. Its
module.qualnamemust resolve back to it, because that is what the driver registry keys on (the credential itself changes constantly). A lambda, a closure or afunctools.partialis rejected; put per-alias configuration incredential_provider_optionsinstead. - It may be called from any thread, and may block. It never runs on a tokio worker. It does run on the calling thread for the initial mint at connect, so a provider should set its own network timeout.
refresh_credential()on the raw client mints immediately, ignoring the schedule — for tests and operators.
TLS is required
A minted credential reaches the server as a cleartext AUTH argument — that
is how ElastiCache IAM auth works, and why AWS requires in-transit encryption
for it. credential_provider therefore refuses a redis:// or valkey://
LOCATION, and a sentinel:// one without ssl_ca_certs. For local
development against a Valkey without TLS, set
credential_provider_allow_plaintext to True; never in production.
AWS ElastiCache
Shipped, so the common case needs no code:
CACHES = {
"default": {
"BACKEND": "django_vcache.backend.ValkeyCache",
# The IAM-enabled ElastiCache user in the userinfo; no password.
"LOCATION": "valkeys://app-iam-user@my-cache-abc123.serverless.use1.cache.amazonaws.com:6379/0",
"OPTIONS": {
"credential_provider": "django_vcache.contrib.aws.elasticache_iam_token",
},
}
}
Install with pip install "django-vcache[aws]" for botocore. AWS credentials
come from the usual chain (instance/task role, AWS_PROFILE, environment).
The token signs the cache name and user id, neither of which is the
hostname. The provider derives them from the endpoint when its shape allows —
serverless (<name>-<id>.serverless.<region>…), cluster-mode configuration
(clustercfg.<name>…) and primary/reader (master.<name>…) endpoints — and
reads the region abbreviation (use1 → us-east-1) from a built-in table.
For a node endpoint, a CNAME, or a region the table does not know, give them
explicitly:
"credential_provider_options": {
"cache_name": "my-cache", # lowercase, as AWS stores it
"region": "us-east-1",
"serverless": True, # adds ResourceType=ServerlessCache to the token
# "user_id": "…", # defaults to the URL userinfo
},
On the AWS side, ElastiCache IAM auth needs Valkey 7.2+ or Redis OSS 7+,
in-transit encryption enabled, an ElastiCache user with authentication
mode iam whose user id equals its user name, that user in a user group
attached to the cache, and an IAM policy granting elasticache:Connect on
both the cache ARN and the user ARN. The token is valid for 15 minutes; the
driver renews at 10 and re-authenticates every live connection, which also
resets ElastiCache's 12-hour connection limit. scripts/elasticache_e2e.py
exercises all of this against a real cache and can soak past both limits.
Environment variables
VCACHE_TOKIO_WORKER_THREADS
Default: 1
Worker-thread count for the driver's tokio runtime. One thread is optimal for the driver's own workload (I/O-bound tasks on a multiplexed connection); raise it only if you push additional CPU-heavy work onto this runtime from Rust.
Applies only to vcache's own runtime. When a downstream extension
installs a runtime provider (set_runtime_provider, see DRIVER_CLASS),
that shared runtime's own configuration governs sizing — for combined
workloads (cache + database pool + app I/O on one pool) a larger,
CPU-count-sized pool is the sensible default, and this variable has no
effect.
VCACHE_COMMAND_RETRIES
Default: 1
How many times a transport failure re-issues a command before the error
reaches the caller. Server errors (WRONGTYPE, OOM, NOSCRIPT) are answers,
not lost connections, and are never retried.
One retry is the default because the failure usually says nothing about the
command that hit it: a pooled connection whose peer went away while it sat idle
(server restart, failover, a load balancer reaping an idle socket) fails on the
first write. For a Django cache backend that lands on a request — a silent miss
with IGNORE_EXCEPTIONS, a 500 without it.
There is no sleep between attempts: the driver reconnects in the background and the retry awaits that reconnect, which carries its own backoff.
Only operations whose repeated application is indistinguishable from a single
application are retried — reads, SET, DEL, EXPIRE, PERSIST, SCAN,
FLUSHDB, SCRIPT LOAD. These are not retried, because a second
application would be observable:
| Not retried | Why |
|---|---|
INCRBY / DECRBY (incr/decr) |
double-counts if the first reply was lost |
SET NX (add) |
the boolean is the result; a retry reports False after a successful set |
| lock acquire / release / extend | same — the boolean is the mutual-exclusion answer |
EVAL / EVALSHA |
arbitrary script side effects |
arbitrary pipelines (pipeline_exec) |
may have partially applied |
LPUSH / RPUSH / RPOP / LREM / LTRIM |
queue semantics; a retry duplicates or over-removes |
BLMOVE / BLMPOP |
a retry can orphan an element into the processing list |
Set to 0 to restore raise-on-first-failure.
Note that a disconnect happening between commands needs no retry at all — the driver reconnects in the background and the next command transparently waits for the new connection. The retry matters for a connection that dies with a command already in flight.
VCACHE_MAX_IDLE_BLOCKING_CONNS
Default: 4
Each in-flight blocking operation (BLMOVE/BLMPOP, used by task queues
such as django-vtasks) holds a dedicated connection, checked out of an idle
pool. This caps how many idle connections are retained for reuse. If you
run more than this many concurrent blocking consumers per process, raise it
to match, or completions beyond the cap will drop and recreate connections
on every cycle.