Configure the Cache and Sessions
core.default_settings.get_defaults requires a CACHE_URL key and builds Django: CACHES["default"] from it, the same way it requires DATABASE_URL. With the scaffolded project's TOML config adapter, startup fails without the key:
KeyError: 'Missing config key: CACHE_URL'This guide covers choosing a value for that key in a deployment. Architecture Overview covers the cache as an infrastructure dependency, alongside PostgreSQL and the message broker.
What the Cache Holds
Three features use this cache, and each one needs every process to reach the same cache:
- Sessions.
get_defaultssets Django: SESSION_ENGINE todjango.contrib.sessions.backends.cache, so a signed-in user's session lives here. - The forgot-password cooldown. VuedaForgotPasswordView accepts one request per address per minute and answers
429to the next. It stores a 60-second marker per lower-cased address. - Rate-limit counters. allauth keeps the count for every one of its rate limits here, and DRF keeps the count for every throttle. Rate-Limited Auth Endpoints lists the ones that apply to VUEDA's endpoints.
On a per-process cache, each process keeps its own counts and markers. A caller then gets the configured limit once per process.
Choose a Backend
Count the processes that serve your application. A production deployment normally runs several web worker processes, so treat the first two rows as the normal case.
| Deployment | CACHE_URL | Needs |
|---|---|---|
| More than one worker process | redis://... | A Redis service and the redis extra |
| More than one worker process, no second service | db://cache_table | One createcachetable run |
| One process only, or local development | locmem:// | Nothing |
Rate-Limited Auth Endpoints
VUEDA limits the auth endpoints that check a password or code, and the ones that send an email or text message. VUEDA enforces some limits with DRF throttles, and allauth enforces the others. The status code of a refused request depends on which one refuses it:
- 400. allauth counts wrong passwords and wrong two-factor codes inside form validation (
login_failed). A refused request gets a 400 response whose body carriesnon_field_errors: ["Too many failed login attempts. Try again later."]. - 429 without
Retry-After. allauth's other limits (loginandreauthenticate) refuse the request before the form runs. The response has noRetry-Afterheader. - 429 with
Retry-After. VUEDA's DRF throttles answer 429 with aRetry-Afterheader that gives the seconds to wait.
Endpoint (under vueda.user/) | What the limit counts | Default | Setting key | Response |
|---|---|---|---|---|
login/ | Every sign-in request | 30 a minute per IP address | ACCOUNT_RATE_LIMITS["login"] | 429, no Retry-After |
login/ | Wrong passwords | 10 a minute per IP address, 5 in 5 minutes per email address | ACCOUNT_RATE_LIMITS["login_failed"] | 400 |
2fa/authenticate/, 2fa/reauthenticate/ | Wrong two-factor codes | 10 a minute per IP address, 5 in 5 minutes per user | ACCOUNT_RATE_LIMITS["login_failed"] | 400 |
reauthenticate/ | Every password confirmation | 10 a minute per user | ACCOUNT_RATE_LIMITS["reauthenticate"] | 429, no Retry-After |
change_password/ | Every password change | 5 a minute per user | REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["change_password"] | 429 with Retry-After |
forgot-password/ | Every reset request | 20 an hour per IP address | REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["forgot_password"] | 429 with Retry-After |
totp_code/, totpdevice/setup/ | Codes sent by email or text message | 5 an hour per user for each method, email or SMS | REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["totp_send_user"] | 429 with Retry-After |
totpdevice/setup/ | Codes sent to a new device's email address or phone number | 5 an hour per email address or phone number | REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["totp_send_destination"] | 429 with Retry-After |
A correct password does not count toward login_failed, and a correct two-factor code clears that user's count. forgot-password/ also refuses a second request for the same address within 60 seconds.
VUEDA and allauth find a client's IP address the same way, through allauth's get_client_ip. It ignores X-Forwarded-For unless you set ALLAUTH_TRUSTED_PROXY_COUNT or ALLAUTH_TRUSTED_CLIENT_IP_HEADER. Behind a reverse proxy, set one of them, or every client shares the proxy's address and its per-IP limits.
To change a VUEDA limit, set its single key in your settings module. Replacing the whole DEFAULT_THROTTLE_RATES dictionary drops VUEDA's other rates, and the endpoints that use them then fail when a request arrives. allauth merges ACCOUNT_RATE_LIMITS over its own defaults, so that dictionary holds only the keys you change. The allauth rate limits reference covers its rate format.
Point CACHE_URL at Redis
Install VUEDA with the redis extra, which installs the Redis client. Django's RedisCache backend imports the client only when the cache is first used. The scaffolded project's server/pyproject.toml already lists the extra:
dependencies = [
"vueda[redis] >=3.0.0a4,<4",
]Set CACHE_URL to the instance's URL:
CACHE_URL = "redis://localhost:6379/0?key_prefix=widgets-"For a unix socket, leave the host empty. For TLS, use the rediss:// scheme:
CACHE_URL = "redis:///var/run/redis/redis.sock?key_prefix=widgets-"
CACHE_URL = "rediss://cache.example.com:6379/0?key_prefix=widgets-"Credentials go in the URL as they do for DATABASE_URL, so keep this key in a configuration layer you do not commit, such as config.local.toml.
Set a Key Prefix
Give every deployment its own key_prefix, which becomes Django's Django: CACHES-KEY_PREFIX. Use the project's own name.
Two applications that share one Redis database and one prefix share their fixed-name keys:
- A forgot-password request in one application starts the cooldown for the same address in the other.
- Rate-limit counts add up across both applications.
Give each application its own database number as well. Django: cache.clear empties the whole Redis database regardless of the key prefix. The call signs out every user of every application in that database. A common split puts the cache on /0 and the Celery broker on /1.
Use the Database Cache Instead
Where a second service is not worth running, point the URL at a table:
CACHE_URL = "db://widgets_cache"The table sits outside the migration graph, so create it once per deployment, after migrating:
python manage.py createcachetableRun it on every new deployment. Without the table, every request that reads or writes the session fails, including sign-in. The forgot-password cooldown and the rate limits fail too. Every process reaches the same table, so sessions hold across workers. The cost is session traffic on your database connection pool.
Keep a Per-Process Cache Only for One Process
locmem:// is private to each process. Use it for manage.py runserver, for a test suite, and for a deployment where one process serves every request:
CACHE_URL = "locmem://unique-snowflake?key_prefix=widgets-"Behind more than one worker, use Redis or the database cache. With locmem://, a session that one worker writes is missing when another worker serves the next request. The user loses the session at an unpredictable point.
Move Sessions Out of the Cache
To keep a per-process cache and still hold sessions across workers, set the database session engine in your own settings module:
SESSION_ENGINE = "django.contrib.sessions.backends.db"The django.contrib.sessions migrations create the table. The forgot-password cooldown and the rate limits still live in the cache. On a per-process cache, the cooldown admits one request per address per worker.
Django: SessionStore (django.contrib.sessions.backends.cached_db) sits between the two. It reads through the cache and writes through to the database. With a per-process cache, cached_db finds fewer sessions in the cache and reads more of them from the database, but it keeps every session.
Verify the Configuration
Run the deploy checks:
python manage.py check --deployWhen sessions use the cache engine and the cache is locmem:// or dummy://, the check reports vueda_core.W001:
WARNINGS:
?: (vueda_core.W001) SESSION_ENGINE stores sessions in the 'default' cache, which uses django.core.cache.backends.locmem.LocMemCache.
HINT: Point CACHE_URL at a cache every worker process shares, such as 'redis://host:6379/0?key_prefix=app-' or 'db://cache_table'. Keep the per-process backend only where one process serves every request, or set SESSION_ENGINE to 'django.contrib.sessions.backends.db'.core.checks.check_session_cache_is_shared runs only with --deploy, and only while Django: DEBUG is off, because a development server is one process. It does not report cached_db.
To confirm a shared backend end to end, write a value in one shell and read it back in another:
python manage.py shell -c "from django.core.cache import cache; cache.set('probe', 'ok', 60)"
python manage.py shell -c "from django.core.cache import cache; print(cache.get('probe'))"Each command is its own process. ok means both reached the same cache, and None means they did not.
Related
- Local HTTPS Development covers running locally over HTTPS, so the secure session cookie works the way it does in production.
- Set Up and Operate the VUEDA Dispatch Queue (VDQ) covers the Celery broker. The broker has its own
CELERY_BROKER_URLkey, even where one Redis instance serves both.