Skip to content

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 AUTH when 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-AUTH on 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_error for a health check. You only get errors once the server actually stops accepting it.
  • The provider must be importable by name. Its module.qualname must resolve back to it, because that is what the driver registry keys on (the credential itself changes constantly). A lambda, a closure or a functools.partial is rejected; put per-alias configuration in credential_provider_options instead.
  • 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 (use1us-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.