DEV Community

Cover image for πŸš€ Why ASGI Over WSGI? πŸ”„ Solving Multi-Device Login Conflicts in Django πŸ“±πŸ’»πŸ›‘οΈ
Bharat Solanke
Bharat Solanke

Posted on

πŸš€ Why ASGI Over WSGI? πŸ”„ Solving Multi-Device Login Conflicts in Django πŸ“±πŸ’»πŸ›‘οΈ

βœ… A practical guide to implementing single-user login session enforcement with real-time logout notifications using Django, ASGI, Channels, WebSockets, and Redis.

πŸ“Œ Problem Statement

We want to ensure that each user can only be logged in on one device at a time.

Hear's the expected behaviour:

  • When a user logs in from a new device, any existing session on other devices should get:
    • ❌ Logged out immediately, and
    • πŸ”” Notified in real-time about the forced logout.

Traditional Django projects (which run on WSGI) can't do this in real-time - you'd need pulling or periodic refresh. We needed an instant notification system

❓ Why WSGI Couldn’t Help

WSGI (Web Server Gateway Interface) is great for traditional request-response cycles but:

❌ WSGI Limitations

  1. No support for WebSockets
  2. No long-lived connections
  3. No built-in async support
  4. Not scalable for concurrent real-time tasks

Hence, we replaced WSGI with ASGI.

βœ… Why We Used ASGI

ASGI (Asynchronous Server Gateway Interface) allows:

  1. βœ… Real-time WebSocket support
  2. βœ… Async communication
  3. βœ… Push-based updates (no polling)
  4. βœ… Integration with channels and channels_redis

This made ASGI a perfect fit for:

  • Real-time session monitoring
  • Force logout via socket
  • Scalable async event-driven features

πŸ“‘ WebSockets (and Why They Matter)

A WebSocket is a protocol that enables bi-directional, full-duplex communication between the server and the client over a single TCP connection.
Unlike HTTP, which is request-response based, WebSockets stay open, so the server can push data to the client without a new request.
In our use case:

When a user logs in from a new device, we:

  • Save the new session key
  • Broadcast a message to the old session’s WebSocket connection
  • The frontend (already connected via WebSocket) receives a real-time message prompting the user

πŸ”§ What We Implemented

βœ… 1. WebSocket Connection Per User
Every logged-in user opens a WebSocket connection. We assign each user to a Redis channel group named uniquely, like user_{user.id}.

# consumers.py

class SessionConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.user = self.scope["user"]
        if self.user.is_authenticated:
            self.group_name = f"user_{self.user.id}"
            await self.channel_layer.group_add(self.group_name, self.channel_name)
            await self.accept()
        else:
            await self.close()

    async def disconnect(self, close_code):
        if self.user.is_authenticated:
            await self.channel_layer.group_discard(self.group_name, self.channel_name)

    async def force_logout(self, event):
        await self.send(text_data=json.dumps({
            "action": "force_logout"
        }))

Enter fullscreen mode Exit fullscreen mode

βœ… 2. Track Current Session ID in DB
We extended Django’s AbstractUser to store the current session key.

# models.py

from django.contrib.auth.models import AbstractUser

class UserMaster(AbstractUser):
    current_session_key = models.CharField(max_length=255, null=True, blank=True)

Enter fullscreen mode Exit fullscreen mode

βœ… 3. Check and Destroy Old Sessions on Login
On login, we check if there is a previous session for the user. If yes:

  • We delete it
  • Notify the old WebSocket channel (group) to logout
# login view

from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync

if user.current_session_key:
    old_session = Session.objects.filter(session_key=user.current_session_key).first()
    if old_session:
        old_session.delete()

    # πŸ”” Notify via WebSocket
    channel_layer = get_channel_layer()
    async_to_sync(channel_layer.group_send)(
        f"user_{user.id}",
        {
            "type": "force_logout",
        }
    )

# Save new session
user.current_session_key = request.session.session_key
user.save()

Enter fullscreen mode Exit fullscreen mode

βœ… 4. Frontend WebSocket Listener
When a logout message is sent, the frontend instantly logs the user out.

// session.js

const socket = new WebSocket('ws://' + window.location.host + '/ws/session/');

socket.onmessage = function(event) {
    const data = JSON.parse(event.data);
    if (data.action === "force_logout") {
        alert("You have been logged out because your account was accessed elsewhere.");
        window.location.href = "/logout/";
    }
};

Enter fullscreen mode Exit fullscreen mode

βš™οΈ Supporting Infrastructure: Redis & Channels
We added these in settings.py:

INSTALLED_APPS += ["channels"]

ASGI_APPLICATION = "your_project.asgi.application"

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("redis", 6379)],
        },
    }
}

Enter fullscreen mode Exit fullscreen mode

🧱 Docker Compose Setup

We added Redis and Uvicorn (ASGI server) to our docker-compose.yml:

version: "3.9"
services:
  web:
    build: .
    command: uvicorn your_project.asgi:application --host 0.0.0.0 --port 8000 --reload
    depends_on:
      - redis
    ports:
      - "8000:8000"
  redis:
    image: redis:alpine
    ports:
      - "6379:6379"

Enter fullscreen mode Exit fullscreen mode

πŸ†š ASGI vs WSGI Summary Table

🧠 Conclusion
With ASGI, we made our Django app:

  • βœ… Real-time capable
  • βœ… WebSocket-enabled
  • βœ… Scalable for future interactive features
  • βœ… More secure with single-session login

This pattern is now ready to be reused for:

  • πŸ”” Notifications
  • πŸ“‘ Live dashboards
  • 🧾 Collaborative forms
  • πŸ‘¨β€πŸ’» Chat or support features

Top comments (0)