Skip to content
Back to skills

Django Redis Caching

ASecurity

Django Redis caching with django-cacheops. This skill should be used when implementing caching, adding cache invalidation, optimizing API performance, modifying models that affect cached data, or debugging cache-related issues in the Django backend.

  • 15 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 4, 2026
developmentpythongodjangodebuggingapibackendperformance

Works with

  • cursor
  • api

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add kettleofketchup/DraftForge --skill django-redis-caching --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Django Redis Caching?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Django Redis Caching
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/kettleofketchup-django-redis-caching/badge)](https://www.skillsdirectory.com/skills/kettleofketchup-django-redis-caching)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: django-redis-caching
description: Django Redis caching with django-cacheops. This skill should be used when implementing caching, adding cache invalidation, optimizing API performance, modifying models that affect cached data, or debugging cache-related issues in the Django backend.
---

# Django Redis Caching Skill

Implements Redis caching with django-cacheops for the DTX Django backend.

## Quick Reference

### Cacheops Configuration

All models use 1-hour cache timeout (configured in `settings.py`):
```python
CACHEOPS = {
    "app.tournament": {"ops": "all", "timeout": 60 * 60},
    "app.team": {"ops": "all", "timeout": 60 * 60},
    "app.customuser": {"ops": "all", "timeout": 60 * 60},
    "app.draft": {"ops": "all", "timeout": 60 * 60},
    "app.game": {"ops": "all", "timeout": 60 * 60},
    "app.draftround": {"ops": "all", "timeout": 60 * 60},
}
```

### View Caching Pattern

```python
from cacheops import cached_as

def list(self, request, *args, **kwargs):
    cache_key = f"model_list:{request.get_full_path()}"

    @cached_as(Model1, Model2, extra=cache_key, timeout=60 * 60)
    def get_data():
        queryset = self.filter_queryset(self.get_queryset())
        serializer = self.get_serializer(queryset, many=True)
        return serializer.data

    return Response(get_data())
```

### Cache Invalidation Pattern

**Preferred: `invalidate_after_commit`** — use inside transactions, signal handlers, or anywhere writes may be deferred:

```python
from app.cache_utils import invalidate_after_commit

with transaction.atomic():
    user.nickname = "New"
    user.save()
    invalidate_after_commit(tournament, org_user, org_user.organization)
```

**Direct `invalidate_obj`** — safe only OUTSIDE transactions (e.g., after M2M `.add()`/`.remove()`):

```python
from cacheops import invalidate_obj

org.admins.add(user)       # M2M auto-commits
invalidate_obj(org)        # Safe — data already committed
```

**When to use which:**

| Context | Use |
|---------|-----|
| Inside `transaction.atomic()` | `invalidate_after_commit()` |
| Inside `@transaction.atomic` decorator | `invalidate_after_commit()` |
| Inside signal handlers (`post_save`, etc.) | `invalidate_after_commit()` |
| After `.save()` / M2M ops outside transactions | Direct `invalidate_obj()` is safe |
| After `.update()` / `.bulk_create()` | `invalidate_after_commit()` (defensive) |

## DTX Model Cache Dependencies

When modifying data, invalidate these related caches:

| Changed Model | Also Invalidate |
|---------------|-----------------|
| DraftRound | Tournament, Draft, Team |
| Draft | Tournament |
| Team | Tournament (if tournament-scoped) |
| Game | Tournament, Team |
| CustomUser | Team (if member changes) |

## Key Principles

1. **Invalidate on Write**: Always invalidate related caches after mutations
2. **Use `invalidate_after_commit`**: Default to deferred invalidation in transactions — see `app/cache_utils.py`
3. **Monitor Dependencies**: Use `@cached_as(Model1, Model2, ...)` to auto-invalidate
4. **Use Specific Keys**: Include request path or pk in cache keys
5. **Keep Fresh for Detail**: Use `keep_fresh=True` for single-object retrieval

## Detailed References

- [Cacheops Patterns](references/cacheops-patterns.md) - Decorator usage, cache keys
- [Invalidation Strategies](references/invalidation-strategies.md) - When and how to invalidate
- [Model Dependencies](references/model-dependencies.md) - DTX model relationships

## Common Operations

### Disable Cache for Management Commands

```python
DISABLE_CACHE=true python manage.py <command>
```

### Manual Cache Invalidation

```python
from cacheops import invalidate_all, invalidate_model, invalidate_obj

# Nuclear option - invalidate everything
invalidate_all()

# Invalidate all instances of a model
invalidate_model(Tournament)

# Invalidate specific instance
invalidate_obj(tournament)
```

### Check if Cache is Working

```python
from django.core.cache import cache
cache.set('test_key', 'test_value', 30)
print(cache.get('test_key'))  # Should print 'test_value'
```

## Timeout Guidelines

| Data Type | Timeout | Reason |
|-----------|---------|--------|
| Static data | 60 * 60 (1h) | Rarely changes |
| Tournament state | 60 * 10 (10m) | Changes during events |
| Draft rounds | 60 * 10 (10m) | Active during drafts |
| Discord guild members (`discord_members_<guild_id>`) | 60 * 60 (1h) | Admin-search-driven refreshes; daily avatar task reads from this same cache. See "Discord Member Cache" below. |
| Other external API short-burst caches | 15-60s | Per-request burst dedup only |

## Discord Member Cache

Single Redis cache for guild members shared by two consumer patterns —
admin-driven and scheduled. **Don't add a parallel cache for the same
data; extend the timeout or call `refresh_discord_members` instead.**

**Cache:** `discord_members_<guild_id>` — full paginated member list
per guild, 1-hour TTL (constant: `DISCORD_MEMBER_CACHE_TTL_S` in
`backend/discordbot/services/users.py`).

**Populated by:**
- `discordbot.services.users.get_discord_members_data(guild_id)` —
  on cache miss, paginates `GET /guilds/{id}/members?limit=1000` via
  the `after` cursor until the guild is exhausted, then writes the
  full list back at TTL.
- `discordbot.services.users.refresh_discord_members(request)` —
  POST endpoint admins hit when they need to add a user who joined
  Discord recently. 5-min per-org cooldown. Force-clears + repaves
  the cache.

**Consumed by:**
- `search_discord_members` — admin-search-by-name on org pages
- `get_organization_discord_members` — full org-member list
- `get_discord_voice_channel_activity` — voice-channel staffing
- `app.tasks.avatar_refresh.refresh_avatars_batched` — daily Celery
  beat. Reads the cache, builds a `discord_id → avatar_hash` map,
  posts diffed rows to the internal endpoint
  `POST /api/internal/users/avatars/bulk-update/` (defined in
  `user/internal/avatar.py`). The endpoint performs
  `bulk_update(batch_size=500)` + `invalidate_model(CustomUser)`
  server-side — the worker never touches the ORM. Daily cadence works
  because admin searches keep the cache current within the day.

**Why no separate avatar cache?** Avatars are stored on `User.avatar`
(DB column), not in Redis. The "avatar cache" IS the DB column; the
daily Celery task is what refreshes it from the live guild member
list. No third cache needed.

**When you'd add a NEW cache key for guild members:** never. Use
`get_discord_members_data`. If a caller needs different semantics
(e.g. force-refresh), use the `refresh_discord_members` endpoint or
`cache.delete(...)` the existing key — don't fork.

### Bulk-update + cacheops invariant

When a process writes thousands of rows via
`Model.objects.bulk_update(...)`, `post_save` doesn't fire and
cacheops won't auto-invalidate. Pair `bulk_update` with one
`invalidate_model(Model)` call after the batched write — N
per-row `invalidate_obj()` calls would defeat the whole point of
batching.

For Celery workers and the Discord bot, the `bulk_update` itself
must live behind an internal HTTP endpoint (`/api/internal/...`)
because workers don't touch the ORM. Reference impl:
`user/internal/avatar.py::bulk_update_user_avatars` —
`bulk_update(batch_size=500)` + `invalidate_model(CustomUser)` in a
single request body. Mirror this shape (validate payload first, then
one `bulk_update`, then one `invalidate_model`) for similar
batched-write paths in other domains.

Files in this skill

  • SKILL.md7.3 KB
  • references/cacheops-patterns.md4.3 KB
  • references/invalidation-strategies.md6.7 KB
  • references/model-dependencies.md4.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…