Django Channels : 3 erreurs qui ont tué mes WebSockets
Lecons & Erreurs

Django Channels : 3 erreurs qui ont tué mes WebSockets

Trois semaines en enfer avant que mes WebSockets tiennent la charge : connexions fantômes qui saturaient la RAM, Redis laissé en configuration par défaut, un Consumer monolithique qui bloquait tout. Les trois erreurs Django Channels et le code exact qui les règle.

2 février 2026
6 min de lecture
Par Tokam Darius

Quand j'ai décidé d'ajouter des notifications en temps réel à Housing AI, mon agrégateur de logements au Québec, je pensais que Django Channels allait être un jeu d'enfant. Après tout, j'avais déjà une solide expérience avec Django et les WebSocket semblaient être la solution évidente pour notifier instantanément les utilisateurs de nouveaux logements correspondant à leurs critères.

Spoiler alert : j'ai passé 3 semaines en enfer.

Entre les connexions qui se multipliaient à l'infini, Redis qui plantait sous la charge et mes Consumers mal architecturés qui créaient des goulots d'étranglement, j'ai failli tout refaire avec FastAPI. Mais j'ai tenu bon, et aujourd'hui je veux partager les 3 erreurs critiques qui ont failli tuer mes WebSockets, et surtout, comment je les ai résolues.

Développeur devant plusieurs écrans affichant des messages d'erreur

L'erreur des connexions fantômes qui bouffent la RAM

Ma première grosse erreur avec Django Channels ? Sous-estimer complètement la gestion des connexions concurrentes. Dans mes tests locaux, tout fonctionnait parfaitement avec 2-3 connexions WebSocket ouvertes. En production, c'était l'apocalypse.

Le problème était vicieux : mes connexions WebSocket ne se fermaient jamais proprement. Chaque fois qu'un utilisateur fermait son onglet ou perdait sa connexion internet, mon Consumer continuait à tourner en arrière-plan comme un zombie. Résultat ? Au bout de quelques heures, j'avais des centaines de connexions fantômes qui bouffaient ma RAM.

python
# Mon Consumer initial (version qui tue la RAM)
class NotificationConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.channel_layer.group_add(
            f"user_{self.user.id}",
            self.channel_name
        )
        await self.accept()

    # Pas de disconnect() propre = connexions fantômes garanties

La solution que j'utilise maintenant ? Un système de heartbeat combiné à un nettoyage automatique. J'ai implémenté un ping/pong toutes les 30 secondes pour détecter les connexions mortes, plus un task Celery qui nettoie les groupes de channels orphelins toutes les heures.

python
# Version corrigée avec gestion des connexions
class NotificationConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.user = self.scope["user"]
        self.group_name = f"user_{self.user.id}"

        await self.channel_layer.group_add(
            self.group_name,
            self.channel_name
        )
        await self.accept()

        # Démarre le heartbeat
        self.heartbeat_task = asyncio.create_task(self.heartbeat_loop())

    async def disconnect(self, close_code):
        # Annule le heartbeat
        if hasattr(self, 'heartbeat_task'):
            self.heartbeat_task.cancel()

        # Nettoie proprement le groupe
        await self.channel_layer.group_discard(
            self.group_name,
            self.channel_name
        )

    async def heartbeat_loop(self):
        while True:
            try:
                await asyncio.sleep(30)
                await self.send(text_data=json.dumps({
                    'type': 'heartbeat'
                }))
            except Exception:
                break

Depuis cette modification, zéro memory leak. Ma consommation RAM est restée stable même avec plus de 500 connexions simultanées.

Redis mal configuré = performance catastrophique

Deuxième erreur monumentale : ma configuration Redis était complètement à côté de la plaque. J'avais juste installé Redis avec les paramètres par défaut et je pensais que ça suffirait. Grosse erreur.

Mes Channel Layers étaient lents, très lents. Une notification qui devrait arriver instantanément prenait parfois 10 à 15 secondes. Mes utilisateurs sur Housing AI recevaient les alertes de nouveaux logements quand c'était déjà trop tard.

Le problème venait de plusieurs configurations Redis foireuses :

1. Pool de connexions insuffisant

python
# Configuration par défaut (limitée)
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

2. Pas de persistence configurée

Redis perdait toutes les connexions actives à chaque redémarrage.

3. Memory policy inadaptée

Redis éjectait mes channels au lieu des anciennes données.

Ma configuration Redis optimisée maintenant :

python
# Configuration Redis optimisée pour Channels
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
            "capacity": 1500,  # Messages max par channel
            "expiry": 60,      # TTL des messages
            "group_expiry": 86400,  # TTL des groupes (24h)
            "symmetric_encryption_keys": [SECRET_KEY],
            # Pool de connexions adapté
            "CONNECTION_KWARGS": {
                "max_connections": 50,
                "retry_on_timeout": True,
                "socket_keepalive": True,
                "socket_keepalive_options": {},
                "health_check_interval": 30,
            }
        },
    },
}

Et dans mon redis.conf :

conf
# Optimisations pour Django Channels
maxmemory 512mb
maxmemory-policy allkeys-lru
tcp-keepalive 60
timeout 300

# Persistence adaptée
save 900 1
save 300 10
save 60 10000

Résultat : mes notifications arrivent maintenant en moins de 200 ms. La différence est spectaculaire.

Architecture Consumer bancale = goulot d'étranglement garanti

Ma troisième erreur, et probablement la plus coûteuse : une architecture de Consumer complètement foireuse qui créait des goulots d'étranglement partout.

Au début, j'avais créé UN seul Consumer pour gérer toutes les notifications de Housing AI : nouveaux logements, mises à jour de prix, alertes de matching, messages du support. Tout passait par le même Consumer avec une énorme fonction receive qui faisait tout.

python
# Le Consumer monstre qui fait tout
class MegaNotificationConsumer(AsyncWebsocketConsumer):
    async def receive(self, text_data):
        data = json.loads(text_data)

        if data['type'] == 'new_listing':
            # 50 lignes de code...
        elif data['type'] == 'price_update':
            # 40 lignes de code...
        elif data['type'] == 'match_alert':
            # 60 lignes de code...
        elif data['type'] == 'support_message':
            # 30 lignes de code...
        # ... et ainsi de suite

Le problème ? Chaque type de notification bloquait les autres. Si le traitement d'une alerte de matching prenait 3 secondes (requête à l'API OpenAI pour scorer les logements), toutes les autres notifications étaient en attente.

D'ailleurs, j'ai déjà parlé de cette tendance à créer des monstres monolithiques dans mon article sur les erreurs Django en production, c'est un piège classique qu'on retrouve partout.

Ma nouvelle architecture ? Un Consumer par type de fonctionnalité avec des tâches asynchrones pour les traitements lourds.

python
# Architecture modulaire avec routing
# websocket_routing.py
websocket_urlpatterns = [
    path('ws/listings/', ListingConsumer.as_asgi()),
    path('ws/alerts/', AlertConsumer.as_asgi()),
    path('ws/support/', SupportConsumer.as_asgi()),
]

# Consumer spécialisé et optimisé
class ListingConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.user = self.scope["user"]
        self.room_group_name = f"listings_user_{self.user.id}"

        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )
        await self.accept()

    async def new_listing_notification(self, event):
        # Traitement léger et rapide
        await self.send(text_data=json.dumps({
            'type': 'new_listing',
            'listing': event['listing_data']
        }))

    async def receive(self, text_data):
        data = json.loads(text_data)

        # Délègue les traitements lourds à Celery
        if data['type'] == 'request_matches':
            process_listing_matches.delay(
                user_id=self.user.id,
                channel_name=self.channel_name
            )

Pour les traitements lourds, j'utilise des tâches Celery asynchrones qui renvoient le résultat via les Channel Layers :

python
# tasks.py
@shared_task
def process_listing_matches(user_id, channel_name):
    # Traitement lourd (IA, calculs complexes...)
    matches = heavy_matching_algorithm(user_id)

    # Renvoie le résultat via WebSocket
    channel_layer = get_channel_layer()
    async_to_sync(channel_layer.send)(channel_name, {
        'type': 'match_results',
        'matches': matches
    })

Cette architecture modulaire a divisé par 10 les temps de réponse et éliminé complètement les blocages.

Poste de travail organisé avec plusieurs écrans affichant des tableaux de bord de données

Ce que j'aurais dû faire dès le début

Avec le recul, ces 3 erreurs auraient pu être évitées si j'avais commencé par les bonnes bases :

1. Toujours commencer petit

J'aurais dû implémenter un seul type de notification d'abord, bien tester la charge, puis étendre progressivement. Comme je l'explique dans mon approche pour créer un MVP rapidement, il faut valider chaque brique avant d'ajouter la suivante.

2. Tester la charge dès le début

Mes tests locaux avec 3 connexions ne valaient rien. J'utilise maintenant websocket-load-test pour simuler plus de 1000 connexions concurrentes dès le développement.

3. Monitorer en temps réel

J'ai ajouté des métriques Prometheus pour surveiller :

  • Nombre de connexions actives
  • Latence des notifications
  • Utilisation mémoire de Redis
  • Taux d'erreur des Consumers

4. Documentation des erreurs

Chaque bug que je résous est documenté avec la solution. Ma « tutorial hell » personnelle m'a appris que la meilleure documentation, c'est celle de tes propres erreurs.

La réalité du WebSocket en production

Aujourd'hui, Housing AI gère plus de 800 connexions WebSocket simultanées sans broncher. Les notifications de nouveaux logements arrivent en moins de 200 ms, et mes utilisateurs reçoivent leurs alertes avant même que les annonces apparaissent sur Kijiji ou LogisQuebec.

Mais soyons honnêtes : Django Channels, c'est pas magique. C'est un outil puissant qui demande une architecture réfléchie et une configuration aux petits oignons. Si tu débutes avec les WebSocket, attends-toi à galérer. C'est normal.

Mon conseil ? Commence par un projet de chat simple avant de te lancer dans quelque chose de complexe. Apprends à gérer les connexions, Redis et les Consumers sur un cas d'usage basique. Une fois que tu maîtrises les bases, tu peux attaquer les vrais projets.

Les WebSocket avec Django, c'est comme conduire une voiture de sport : c'est puissant, mais ça pardonne pas les erreurs de pilotage.

Tags

API
Django
Erreurs
MVP
Python
Validation

A lire aussi

T

Tokam Darius

Développeur web à Jonquière, au Saguenay. Je conçois et je code des applications web sur mesure en Django, React et TypeScript.