Preparation

Setting Up Your Environment

Before we start building our WebSocket application, let’s set up the development environment with all the necessary tools and dependencies.

Prerequisites

Make sure you have the following installed:

  • Python 3.11+ - Fast Channels requires Python 3.11 or higher

  • Docker - For running Redis locally

  • uv (recommended) or pip - For package management

Creating Your Project

First, create a new directory for your tutorial project:

mkdir tutorial-project
cd tutorial-project

Now create the main project structure with a sandbox directory for your source code:

mkdir sandbox

This structure matches the Fast Channels repository layout, where sandbox is the source directory at the same level as pyproject.toml.

Installing Dependencies

You’ll need several packages for this tutorial. Choose your preferred package manager:

Option 1: Using uv (Recommended)

# Create a virtual environment with seed packages
uv venv --seed

# Activate the virtual environment
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Add the required dependencies
uv add fastapi "fast-channels[redis]" rq "uvicorn[standard]"

Option 2: Using pip

# Create a virtual environment
python -m venv .venv

# Activate it
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Install dependencies
pip install fastapi "fast-channels[redis]" rq "uvicorn[standard]"

Required Dependencies Explained:

  • fastapi - Modern ASGI web framework for building APIs

  • fast-channels[redis] - Fast Channels with Redis channel layer support

  • rq - Redis Queue for background job processing

  • uvicorn[standard] - ASGI server for running FastAPI applications

Setting Up Redis with Docker

Redis is required for channel layers in production. We’ll use Docker Compose to run Redis locally.

Create a docker-compose.yml file in your project root directory (tutorial-project/):

services:
  redis:
    image: redis:7
    ports:
      - "6399:6379"
    networks:
      - fast-channels

networks:
  fast-channels:
    driver: bridge

Note: We’re using port 6399 instead of the default 6379 to avoid conflicts with any existing Redis installations.

Start Redis:

docker compose up -d

You can verify Redis is running:

docker compose ps

Project Structure

Create the basic project structure:

mkdir -p sandbox/static/css sandbox/static/js
touch sandbox/__init__.py sandbox/main.py

Your project structure should look like this:

tutorial-project/
├── docker-compose.yml
└── sandbox/
    ├── __init__.py
    ├── main.py
    └── static/
        ├── css/
        └── js/

Basic FastAPI Application

Create a basic FastAPI application in sandbox/main.py:

"""
FastAPI WebSocket Chat Demo - Template Base

This template provides a starting point for building WebSocket chat applications
with fast-channels. Follow the comments to add your own consumers and features.
"""

from fastapi import APIRouter, FastAPI
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles

# TODO: Import your consumers here (uncomment as needed)
# from sandbox.apps.system_chat.consumer import SystemMessageConsumer
# from sandbox.apps.room_chat.consumer import RoomChatConsumer
# from sandbox.apps.background_jobs.consumer import BackgroundJobConsumer
# from sandbox.apps.showcase.consumer import (
#     AnalyticsConsumer,
#     ChatConsumer,
#     NotificationConsumer,
#     ReliableChatConsumer,
# )

# TODO: Import and setup your channel layers
# from sandbox.layers import setup_layers
# setup_layers()

app = FastAPI()

# Mount static files
app.mount("/static", StaticFiles(directory="sandbox/static"), name="static")

# ----------------- HTML Client -----------------
html = """
<!DOCTYPE html>
<html>
    <head>
        <title>WebSocket Chat Demo</title>
        <link rel="stylesheet" href="/static/css/style.css">
    </head>
    <body>
        <h1>WebSocket Chat Demo</h1>
        <button class="analytics-btn" onclick="sendAnalyticsEvent()">Send Analytics Event</button>

        <div class="chat-container">
            <!-- System Messages Chat Box -->
            <div class="chat-box system-chat">
                <h3>System Messages (No Layer)</h3>
                <form class="input-form" onsubmit="sendSystemMessage(event)">
                    <input type="text" id="systemMessageText" placeholder="Type system message..." autocomplete="off"/>
                    <button type="submit">Send</button>
                </form>
                <ul id='systemMessages' class='messages'></ul>
            </div>

            <!-- Room Chat Box -->
            <div class="chat-box room-chat">
                <h3>Room Chat</h3>
                <div class="room-controls">
                    <input type="text" id="roomName" placeholder="Enter room name..." autocomplete="off"/>
                    <button onclick="connectToRoom()" id="connectBtn">Connect</button>
                    <button onclick="disconnectFromRoom()" id="disconnectBtn" style="display:none;">Disconnect</button>
                </div>
                <div id="currentRoom" style="margin: 10px 0; font-weight: bold;"></div>
                <form class="input-form" onsubmit="sendRoomMessage(event)">
                    <input type="text" id="roomMessageText" placeholder="Type room message..." autocomplete="off" disabled/>
                    <button type="submit" disabled>Send</button>
                </form>
                <ul id='roomMessages' class='messages'></ul>
            </div>

            <!-- Background Job Processing Chat Box -->
            <div class="chat-box job-chat">
                <h3>Background Job Processing</h3>
                <div class="job-controls">
                    <select id="jobType">
                        <option value="default">Default Processing</option>
                        <option value="translate">Translation</option>
                        <option value="analyze">Text Analysis</option>
                        <option value="generate">AI Generation</option>
                    </select>
                </div>
                <form class="input-form" onsubmit="sendJobMessage(event)">
                    <input type="text" id="jobMessageText" placeholder="Type message for processing..." autocomplete="off"/>
                    <button type="submit">Process</button>
                </form>
                <ul id='jobMessages' class='messages'></ul>
            </div>

            <!-- All Layers Combination Chat Box -->
            <div class="chat-box regular-chat">
                <h3>Showcase</h3>
                <form class="input-form" onsubmit="sendMessage(event)">
                    <input type="text" id="messageText" placeholder="Type message..." autocomplete="off"/>
                    <button type="submit">Send</button>
                </form>
                <ul id='messages' class='messages'></ul>
            </div>
        </div>

        <script src="/static/js/main.js"></script>
    </body>
</html>
"""

home_router = APIRouter(tags=["home"])


@home_router.get("/")
async def home():
    return HTMLResponse(html)


app.include_router(home_router)

# ----------------- WebSocket Sub-App -----------------
ws_router = FastAPI()

# TODO: Add your WebSocket routes here
# Example routes:
# ws_router.add_websocket_route("/system", SystemMessageConsumer.as_asgi())
# ws_router.add_websocket_route("/room/{room_name}", RoomChatConsumer.as_asgi())
# ws_router.add_websocket_route("/backgroundjob", BackgroundJobConsumer.as_asgi())
# ws_router.add_websocket_route("/chat", ChatConsumer.as_asgi())
# ws_router.add_websocket_route("/reliable", ReliableChatConsumer.as_asgi())
# ws_router.add_websocket_route("/notifications", NotificationConsumer.as_asgi())
# ws_router.add_websocket_route("/analytics", AnalyticsConsumer.as_asgi())

app.mount("/ws", ws_router)

This creates a basic FastAPI application with static file serving and a simple HTML page to get you started.

Adding JavaScript Functionality

Create the JavaScript file at sandbox/static/js/main.js to handle WebSocket connections and user interactions:

JavaScript content
// FastAPI WebSocket Chat Demo - Main JavaScript
// All functionality combined in a single file for simplicity

// =============================================================================
// WebSocket Connections
// =============================================================================

// WebSocket connections for different layer types
var wsChat = new WebSocket("ws://localhost:8080/ws/chat");
var wsNotifications = new WebSocket("ws://localhost:8080/ws/notifications");
var wsReliable = new WebSocket("ws://localhost:8080/ws/reliable");
var wsAnalytics = new WebSocket("ws://localhost:8080/ws/analytics");
var wsSystem = new WebSocket("ws://localhost:8080/ws/system");
var wsBackgroundJob = new WebSocket("ws://localhost:8080/ws/backgroundjob");
var wsRoom = null; // Dynamic room connection

// =============================================================================
// WebSocket Event Handlers
// =============================================================================

// Handle chat messages
wsChat.onmessage = function(event) {
    addMessage("Chat: " + event.data, "chat");
};

// Handle notifications (JSON format)
wsNotifications.onmessage = function(event) {
    try {
        var data = JSON.parse(event.data);
        addMessage("Notification: " + data.message, "notification");
    } catch (e) {
        // Fallback for non-JSON messages
        addMessage("Notification: " + event.data, "notification");
    }
};

// Handle reliable messages
wsReliable.onmessage = function(event) {
    addMessage("Reliable: " + event.data, "reliable");
};

// Handle analytics messages
wsAnalytics.onmessage = function(event) {
    addMessage("Analytics: " + event.data, "analytics");
};

// Handle system messages
wsSystem.onmessage = function(event) {
    addSystemMessage(event.data);
};

// Handle background job messages (JSON format)
wsBackgroundJob.onmessage = function(event) {
    try {
        var data = JSON.parse(event.data);
        var message = data.message || event.data;

        // Add status indicator for different types of responses
        if (data.status === "queuing") {
            addJobMessage("⏳ " + message);
        } else if (data.status === "queued") {
            addJobMessage("📋 " + message + " (ID: " + data.job_id + ")");
        } else if (data.status === "error") {
            addJobMessage("❌ " + message);
        } else if (data.type === "job_result") {
            addJobMessage("✅ " + message);
        } else {
            addJobMessage(message);
        }
    } catch (e) {
        // Fallback for non-JSON messages
        addJobMessage(event.data);
    }
};

// =============================================================================
// Utility Functions - Message Display
// =============================================================================

function addMessage(text, type) {
    var messages = document.getElementById('messages');
    var message = document.createElement('li');
    message.className = type;
    var content = document.createTextNode(text);
    message.appendChild(content);
    messages.appendChild(message);
    messages.scrollTop = messages.scrollHeight;
}

function addSystemMessage(text, isUserMessage = false) {
    var messages = document.getElementById('systemMessages');
    var message = document.createElement('li');
    message.className = isUserMessage ? 'user-message' : 'system';
    var content = document.createTextNode(text);
    message.appendChild(content);
    messages.appendChild(message);
    messages.scrollTop = messages.scrollHeight;
}

function addRoomMessage(text) {
    var messages = document.getElementById('roomMessages');
    var message = document.createElement('li');
    message.className = 'room';
    var content = document.createTextNode(text);
    message.appendChild(content);
    messages.appendChild(message);
    messages.scrollTop = messages.scrollHeight;
}

function addJobMessage(text) {
    var messages = document.getElementById('jobMessages');
    var message = document.createElement('li');
    message.className = 'job';
    var content = document.createTextNode(text);
    message.appendChild(content);
    messages.appendChild(message);
    messages.scrollTop = messages.scrollHeight;
}

// =============================================================================
// System Chat Functions
// =============================================================================

function sendSystemMessage(event) {
    var input = document.getElementById("systemMessageText");
    var message = input.value;

    if (message.trim() === '') return;

    // Show user message first
    addSystemMessage("👤 User: " + message, true);

    // Send to system WebSocket connection
    if (wsSystem.readyState === WebSocket.OPEN) {
        wsSystem.send(message);
    }

    input.value = '';
    event.preventDefault();
}

// =============================================================================
// Room Chat Functions
// =============================================================================

function connectToRoom() {
    var roomName = document.getElementById("roomName").value.trim();
    if (roomName === '') {
        alert('Please enter a room name');
        return;
    }

    // Close existing room connection if any
    if (wsRoom) {
        wsRoom.close();
    }

    // Create new room WebSocket connection
    wsRoom = new WebSocket("ws://localhost:8080/ws/room/" + roomName);

    wsRoom.onopen = function() {
        document.getElementById("currentRoom").textContent = "Connected to room: " + roomName;
        document.getElementById("connectBtn").style.display = "none";
        document.getElementById("disconnectBtn").style.display = "inline";
        document.getElementById("roomMessageText").disabled = false;
        document.querySelector("#roomMessageText").nextElementSibling.disabled = false;
    };

    wsRoom.onmessage = function(event) {
        addRoomMessage(event.data);
    };

    wsRoom.onclose = function() {
        document.getElementById("currentRoom").textContent = "";
        document.getElementById("connectBtn").style.display = "inline";
        document.getElementById("disconnectBtn").style.display = "none";
        document.getElementById("roomMessageText").disabled = true;
        document.querySelector("#roomMessageText").nextElementSibling.disabled = true;
    };
}

function disconnectFromRoom() {
    if (wsRoom) {
        wsRoom.close();
        wsRoom = null;
    }
}

function sendRoomMessage(event) {
    var input = document.getElementById("roomMessageText");
    var message = input.value;

    if (wsRoom && wsRoom.readyState === WebSocket.OPEN && message.trim() !== '') {
        wsRoom.send(message);
    }

    input.value = '';
    event.preventDefault();
}

// =============================================================================
// Background Jobs Functions
// =============================================================================

function sendJobMessage(event) {
    var input = document.getElementById("jobMessageText");
    var message = input.value;
    var jobType = document.getElementById("jobType").value;

    if (message.trim() === '') return;

    if (wsBackgroundJob.readyState === WebSocket.OPEN) {
        // Send structured message for different job types
        var jobData = {
            type: jobType,
            content: message
        };
        wsBackgroundJob.send(JSON.stringify(jobData));
    }

    input.value = '';
    event.preventDefault();
}

// =============================================================================
// Showcase Functions (All Layers)
// =============================================================================

function sendMessage(event) {
    var input = document.getElementById("messageText");
    var message = input.value;

    // Send to all WebSocket connections
    if (wsChat.readyState === WebSocket.OPEN) {
        wsChat.send(message);
    }
    if (wsReliable.readyState === WebSocket.OPEN) {
        wsReliable.send(message);
    }

    input.value = '';
    event.preventDefault();
}

// =============================================================================
// Analytics Functions
// =============================================================================

function sendAnalyticsEvent() {
    if (wsAnalytics.readyState === WebSocket.OPEN) {
        var event = {
            type: "page_view",
            timestamp: new Date().toISOString(),
            user_agent: navigator.userAgent,
            url: window.location.href
        };
        wsAnalytics.send(JSON.stringify(event));
    }
}

// =============================================================================
// Initialization
// =============================================================================

// Send analytics event on page load
window.onload = function() {
    setTimeout(function() {
        sendAnalyticsEvent();
    }, 1000); // Wait 1 second for connection
};

This JavaScript file provides all the WebSocket connection handling, message sending, and UI interactions needed for the tutorial application.

Adding Basic Styles

Create a basic CSS file at sandbox/static/css/style.css:

CSS content
* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    background: #fafafa;
    padding: 20px;
    color: #333;
}

h1 {
    text-align: center;
    color: #2d3748;
    font-size: 2em;
    margin-bottom: 20px;
    font-weight: 600;
}

.analytics-btn {
    display: block;
    margin: 0 auto 30px;
    background: #4a90e2;
    color: white;
    border: none;
    padding: 10px 20px;
    border-radius: 6px;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    transition: background-color 0.2s;
}

.analytics-btn:hover {
    background: #357abd;
}

.chat-container {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 20px;
    max-width: 1400px;
    margin: 0 auto;
}

.chat-box {
    background: white;
    border-radius: 8px;
    padding: 20px;
    border: 1px solid #e0e0e0;
    box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}

.system-chat {
    border-top: 3px solid #10b981;
}

.regular-chat {
    border-top: 3px solid #4a90e2;
}

.room-chat {
    border-top: 3px solid #e74c3c;
}

.job-chat {
    border-top: 3px solid #9b59b6;
}

.chat-box h3 {
    margin-bottom: 16px;
    font-size: 1.1em;
    font-weight: 600;
}

.system-chat h3 {
    color: #10b981;
}

.regular-chat h3 {
    color: #4a90e2;
}

.room-chat h3 {
    color: #e74c3c;
}

.job-chat h3 {
    color: #9b59b6;
}

.input-form {
    display: flex;
    gap: 8px;
    margin-bottom: 16px;
}

.input-form input {
    flex: 1;
    padding: 10px 12px;
    border: 1px solid #ddd;
    border-radius: 6px;
    font-size: 14px;
    outline: none;
    transition: border-color 0.2s;
}

.input-form input:focus {
    border-color: #4a90e2;
}

.system-chat .input-form input:focus {
    border-color: #10b981;
}

.input-form button {
    padding: 10px 16px;
    background: #4a90e2;
    color: white;
    border: none;
    border-radius: 6px;
    font-weight: 500;
    cursor: pointer;
    transition: background-color 0.2s;
}

.system-chat .input-form button {
    background: #10b981;
}

.input-form button:hover {
    background: #357abd;
}

.system-chat .input-form button:hover {
    background: #059669;
}

.messages {
    height: 280px;
    overflow-y: auto;
    background: #f9f9f9;
    border-radius: 6px;
    padding: 12px;
    border: 1px solid #e0e0e0;
}

.messages::-webkit-scrollbar {
    width: 4px;
}

.messages::-webkit-scrollbar-track {
    background: #f1f1f1;
}

.messages::-webkit-scrollbar-thumb {
    background: #ccc;
    border-radius: 2px;
}

.messages li {
    list-style: none;
    padding: 8px 12px;
    margin: 6px 0;
    border-radius: 8px;
    max-width: 80%;
    word-wrap: break-word;
    font-size: 14px;
}

.system {
    background: #10b981;
    color: white;
    font-weight: 500;
    margin-left: auto;
}

.user-message {
    background: #e9ecef;
    color: #495057;
    margin-right: auto;
}

.chat {
    background: #4a90e2;
    color: white;
    margin-left: auto;
}

.notification {
    background: #f39c12;
    color: white;
    margin-left: auto;
}

.reliable {
    background: #27ae60;
    color: white;
    margin-left: auto;
}

.analytics {
    background: #9b59b6;
    color: white;
    margin-left: auto;
}

.room {
    background: #e74c3c;
    color: white;
    margin-left: auto;
}

.job {
    background: #9b59b6;
    color: white;
    margin-left: auto;
}

.room-controls, .job-controls {
    display: flex;
    gap: 8px;
    margin-bottom: 16px;
    align-items: center;
}

.room-controls input {
    flex: 1;
    padding: 8px 12px;
    border: 1px solid #ddd;
    border-radius: 6px;
    font-size: 14px;
}

.room-controls button, .job-controls select {
    padding: 8px 12px;
    border: 1px solid #ddd;
    border-radius: 6px;
    font-size: 14px;
    cursor: pointer;
}

.room-controls button {
    background: #e74c3c;
    color: white;
    border: none;
}

.room-controls button:hover {
    background: #c0392b;
}

.job-controls select {
    background: white;
}

@media (max-width: 768px) {
    .chat-container {
        grid-template-columns: 1fr;
        gap: 16px;
    }

    body {
        padding: 12px;
    }

    h1 {
        font-size: 1.6em;
    }
}

This provides all the styles needed for the tutorial application including responsive layout, chat boxes, input forms, and message styling.

Setting Up Channel Layers

Channel layers enable cross-process communication and group messaging in Fast Channels. Let’s set them up now so they’re ready when we need them in later tutorial sections.

Create a sandbox/layers.py file:

# sandbox/layers.py
"""
Channel layer definitions and registration.
This file centralizes all channel layer configuration for the application.
"""

import os

from fast_channels.layers import (
    InMemoryChannelLayer,
    has_layers,
    register_channel_layer,
)
from fast_channels.layers.redis import (
    RedisChannelLayer,
    RedisPubSubChannelLayer,
)

def setup_channel_layers():
    """
    Set up and register all channel layers for the application.
    This should be called once during application startup.
    """
    # Prevent duplicate registration
    if has_layers():
        return

    # Get Redis URL from environment or use default
    redis_url = os.getenv("REDIS_URL", "redis://localhost:6399")

    # Create different types of layers for different use cases
    layers_config = {
        # In-memory layer for development/testing
        "memory": InMemoryChannelLayer(),

        # Redis Pub/Sub layer for real-time messaging (low latency)
        "chat": RedisPubSubChannelLayer(
            hosts=[redis_url],
            prefix="chat"
        ),

        # Redis Queue layer for reliable messaging (guaranteed delivery)
        "queue": RedisChannelLayer(
            hosts=[redis_url],
            prefix="queue",
            expiry=900,    # 15 minutes
            capacity=1000,
        ),

        # Notifications layer
        "notifications": RedisPubSubChannelLayer(
            hosts=[redis_url],
            prefix="notify"
        ),

        # Analytics layer for metrics/events
        "analytics": RedisChannelLayer(
            hosts=[redis_url],
            prefix="analytics",
            expiry=3600,   # 1 hour
            capacity=5000,
        ),
    }

    # Register all layers
    for alias, layer in layers_config.items():
        register_channel_layer(alias, layer)

Understanding the Layer Types:

  • memory: For testing and development (single-process only)

  • chat: For real-time chat (Redis Pub/Sub - low latency, no persistence)

  • queue: For reliable messaging (Redis Queue - persistent, guaranteed delivery)

  • notifications: For real-time notifications (Redis Pub/Sub)

  • analytics: For analytics events (Redis Queue with longer expiry)

Note: The Redis URL uses port 6399 to match our Docker Compose configuration. For detailed information about channel layers, see the Channel Layer Setup Guide guide.

Testing Your Setup

Let’s test that everything is working:

  1. Start your FastAPI application:

uvicorn sandbox.main:app --reload --port 8080
  1. Visit your application:

Open your browser and go to http://localhost:8080. You should see your basic FastAPI application.

Tutorial preparation setup result

Troubleshooting

Common Issues:

Port Already in Use:

If port 8080 is busy, change the port in the uvicorn command: uvicorn sandbox.main:app --reload --port 8000. You’ll also need to update the WebSocket URLs in sandbox/static/js/main.js to match the new port (e.g., change ws://localhost:8080 to ws://localhost:8000)

Redis Connection Issues:

Make sure Docker is running and the Redis container started successfully with docker compose ps

Import Errors:

Ensure you’re in the correct directory and your virtual environment is activated

Docker Permission Issues:

On Linux, you might need to run Docker commands with sudo or add your user to the docker group

Next Steps

Great! You now have:

✅ A working FastAPI application

✅ Fast Channels installed with Redis support

✅ Redis running in Docker

✅ Basic project structure and styles

You’re ready to start building WebSocket consumers! In the next section, we’ll create our first WebSocket consumer without channel layers to understand the basics.

Continue to System Messages to build your first WebSocket consumer.