Handle a WebSocket disconnect in a Django Channels consumer

A WebSocket connection can disappear while its consumer still owns group membership or a background coroutine. Put cleanup in the disconnect path and make it safe when connection setup completed only partly. Reject unauthenticated connections before accepting them or joining a private group.
Pair a join with a guarded leave
This illustrative Channels consumer assumes authentication middleware, a configured group-capable channel layer and a correctly wired route. It was reviewed without running Channels in the authoring environment.
from channels.generic.websocket import AsyncWebsocketConsumer
class StatusConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.joined = False
user = self.scope.get("user")
if user is None or not user.is_authenticated:
await self.close()
return
self.group = f"status.user.{user.pk}"
await self.channel_layer.group_add(self.group, self.channel_name)
self.joined = True
await self.accept()
async def disconnect(self, close_code):
if getattr(self, "joined", False):
await self.channel_layer.group_discard(self.group, self.channel_name)
self.joined = False
Group names also need to meet the configured backend's restrictions. The example uses an authenticated user's identifier rather than trusting a browser-supplied group name. Authorization can still require additional checks if events concern a shared workspace or a particular job.
Only an authenticated connection joins the authorized group; disconnect releases membership if the join succeeded.
Clean up work you started
If the consumer starts a background coroutine, retain its task reference and cancel and await it during disconnect. Bound shutdown work so that it can finish within the server's close timeout. Do not let a cleanup exception prevent other owned resources from being released.
Channels distinguishes generic consumers from lower-level consumers. Generic WebSocket classes handle the normal stop behavior; a custom low-level consumer has to manage its own termination after cleanup. Avoid copying a low-level StopConsumer pattern into every generic handler without understanding that lifecycle.
Exercise unexpected closes
Test a normal browser close, a failed authentication attempt and a connection dropped after group membership was established. Confirm that repeated cleanup remains harmless and that no task keeps sending after disconnect. A reconnect should establish its own fresh authorized membership.
Disconnect handling does not guarantee delivery of events that occurred while the browser was offline. Persistent event history and replay need a separate application design. Keep subscription authorization separate from replay authorization when a user reconnects with a saved position. Reference: Channels consumers. Related async-workflow examples appear in the engineering blog.