- Real-Time WebSocket Architecture Series: Part 1 – Understanding WebSocket Fundamentals
- Real-Time WebSocket Architecture Series: Part 2 – Building Your First WebSocket Server (Node.js)
- Real-Time WebSocket Architecture Series: Part 3 – Essential Features (Rooms, Namespaces & Events)
- Real-Time WebSocket Architecture Series: Part 4 – Authentication & Security
- Real-Time WebSocket Architecture Series: Part 5 – Scaling with Redis
- Real-Time WebSocket Architecture Series: Part 6 – Production-Grade Features
- Real-Time WebSocket Architecture Series: Part 7 – Serverless WebSocket Implementation with AWS Lambda
- Real-Time WebSocket Architecture Series: Part 8 – Edge Computing WebSockets with Ultra-Low Latency
Welcome back to our Real-Time WebSocket Architecture series! In previous parts, we’ve built traditional WebSocket servers and explored scaling strategies. Today, we’re diving into the cutting-edge world of serverless WebSocket architecture using AWS API Gateway and Lambda functions.
Serverless architecture has become a game-changer in 2025, with Node.js being perfectly suited for serverless environments due to its lightweight runtime and event-driven architecture. Platforms like AWS Lambda have optimized their serverless offerings for Node.js, enabling developers to deploy lightweight functions that scale automatically.
Why Serverless WebSockets?
With serverless architecture, developers can focus solely on writing code that responds to events or triggers, while the cloud service provider takes care of resource allocation, scaling, and maintenance. This approach offers several compelling advantages:
- Pay-per-use billing – Only pay for actual WebSocket connections and message processing
- Automatic scaling – Handle traffic spikes without infrastructure management
- Zero server maintenance – No more patching, updating, or monitoring servers
- Global edge deployment – Reduced latency with AWS edge locations
graph TB
    Client[WebSocket Client] --> API[API Gateway WebSocket]
    API --> Lambda1[Connect Handler]
    API --> Lambda2[Message Handler] 
    API --> Lambda3[Disconnect Handler]
    Lambda1 --> DDB[(DynamoDBConnection Store)]
    Lambda2 --> DDB
    Lambda3 --> DDB
    Lambda2 --> SQS[SQS Queue]
    SQS --> Lambda4[Broadcast Handler]
    Lambda4 --> API
    Lambda4 --> DDBArchitecture Overview
Our serverless WebSocket architecture consists of four main components:
- API Gateway WebSocket API – Manages WebSocket connections
- Lambda Functions – Handle connect, disconnect, and message events
- DynamoDB – Stores connection metadata and application state
- SQS – Handles message broadcasting and queuing
Setting Up the Project
Let’s start by creating our serverless WebSocket chat application using the Serverless Framework:
# Install Serverless Framework globally
npm install -g serverless
# Create new project
serverless create --template aws-nodejs --path serverless-websocket-chat
cd serverless-websocket-chat
# Install dependencies
npm init -y
npm install aws-sdk uuidCreate the serverless.yml configuration:
service: serverless-websocket-chat
provider:
  name: aws
  runtime: nodejs18.x
  region: us-east-1
  environment:
    CONNECTIONS_TABLE: ${self:service}-connections-${self:provider.stage}
    MESSAGES_QUEUE: ${self:service}-messages-${self:provider.stage}
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:PutItem
            - dynamodb:GetItem
            - dynamodb:DeleteItem
            - dynamodb:Scan
          Resource:
            - Fn::GetAtt: [ConnectionsTable, Arn]
        - Effect: Allow
          Action:
            - sqs:SendMessage
            - sqs:ReceiveMessage
            - sqs:DeleteMessage
          Resource:
            - Fn::GetAtt: [MessagesQueue, Arn]
        - Effect: Allow
          Action:
            - execute-api:PostToConnection
          Resource: "*"
functions:
  connectHandler:
    handler: handlers/connect.handler
    events:
      - websocket:
          route: $connect
  
  disconnectHandler:
    handler: handlers/disconnect.handler
    events:
      - websocket:
          route: $disconnect
  
  messageHandler:
    handler: handlers/message.handler
    events:
      - websocket:
          route: message
  
  broadcastHandler:
    handler: handlers/broadcast.handler
    events:
      - sqs:
          arn:
            Fn::GetAtt: [MessagesQueue, Arn]
resources:
  Resources:
    ConnectionsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.CONNECTIONS_TABLE}
        AttributeDefinitions:
          - AttributeName: connectionId
            AttributeType: S
        KeySchema:
          - AttributeName: connectionId
            KeyType: HASH
        BillingMode: PAY_PER_REQUEST
    
    MessagesQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: ${self:provider.environment.MESSAGES_QUEUE}Connection Handler
Create handlers/connect.js to manage new WebSocket connections:
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');
const dynamodb = new AWS.DynamoDB.DocumentClient();
exports.handler = async (event) => {
  const { connectionId } = event.requestContext;
  const { username } = event.queryStringParameters || { username: 'Anonymous' };
  
  const timestamp = new Date().toISOString();
  
  try {
    // Store connection in DynamoDB
    await dynamodb.put({
      TableName: process.env.CONNECTIONS_TABLE,
      Item: {
        connectionId,
        username,
        connectedAt: timestamp,
        userId: uuidv4()
      }
    }).promise();
    
    console.log(`User ${username} connected with ID: ${connectionId}`);
    
    return {
      statusCode: 200,
      body: JSON.stringify({
        message: 'Connected successfully',
        connectionId,
        username
      })
    };
  } catch (error) {
    console.error('Connection error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({
        message: 'Failed to connect',
        error: error.message
      })
    };
  }
};Message Handler
Create handlers/message.js to process incoming messages:
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
const sqs = new AWS.SQS();
exports.handler = async (event) => {
  const { connectionId } = event.requestContext;
  const { body } = event;
  
  try {
    const messageData = JSON.parse(body);
    const { message, type = 'chat' } = messageData;
    
    // Get user info from DynamoDB
    const connection = await dynamodb.get({
      TableName: process.env.CONNECTIONS_TABLE,
      Key: { connectionId }
    }).promise();
    
    if (!connection.Item) {
      return {
        statusCode: 404,
        body: JSON.stringify({ message: 'Connection not found' })
      };
    }
    
    const { username, userId } = connection.Item;
    
    // Prepare message for broadcasting
    const broadcastMessage = {
      type,
      message,
      username,
      userId,
      timestamp: new Date().toISOString(),
      senderConnectionId: connectionId
    };
    
    // Send to SQS for broadcasting
    await sqs.sendMessage({
      QueueUrl: `https://sqs.${process.env.AWS_REGION}.amazonaws.com/${process.env.AWS_ACCOUNT_ID}/${process.env.MESSAGES_QUEUE}`,
      MessageBody: JSON.stringify(broadcastMessage)
    }).promise();
    
    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'Message sent successfully' })
    };
  } catch (error) {
    console.error('Message handling error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({
        message: 'Failed to send message',
        error: error.message
      })
    };
  }
};Broadcast Handler
Create handlers/broadcast.js to send messages to all connected clients:
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
const apiGateway = new AWS.ApiGatewayManagementApi({
  endpoint: process.env.WEBSOCKET_API_ENDPOINT
});
exports.handler = async (event) => {
  try {
    for (const record of event.Records) {
      const messageData = JSON.parse(record.body);
      
      // Get all active connections
      const connections = await dynamodb.scan({
        TableName: process.env.CONNECTIONS_TABLE
      }).promise();
      
      // Broadcast to all connections except sender
      const broadcastPromises = connections.Items
        .filter(conn => conn.connectionId !== messageData.senderConnectionId)
        .map(async (connection) => {
          try {
            await apiGateway.postToConnection({
              ConnectionId: connection.connectionId,
              Data: JSON.stringify({
                type: 'message',
                data: messageData
              })
            }).promise();
          } catch (error) {
            // Remove stale connections
            if (error.statusCode === 410) {
              await dynamodb.delete({
                TableName: process.env.CONNECTIONS_TABLE,
                Key: { connectionId: connection.connectionId }
              }).promise();
            }
          }
        });
      
      await Promise.all(broadcastPromises);
    }
    
    return { statusCode: 200 };
  } catch (error) {
    console.error('Broadcast error:', error);
    return { statusCode: 500 };
  }
};Disconnect Handler
Create handlers/disconnect.js to clean up connections:
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
exports.handler = async (event) => {
  const { connectionId } = event.requestContext;
  
  try {
    // Get user info before deletion
    const connection = await dynamodb.get({
      TableName: process.env.CONNECTIONS_TABLE,
      Key: { connectionId }
    }).promise();
    
    // Remove connection from DynamoDB
    await dynamodb.delete({
      TableName: process.env.CONNECTIONS_TABLE,
      Key: { connectionId }
    }).promise();
    
    if (connection.Item) {
      console.log(`User ${connection.Item.username} disconnected`);
    }
    
    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'Disconnected successfully' })
    };
  } catch (error) {
    console.error('Disconnect error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({
        message: 'Failed to disconnect',
        error: error.message
      })
    };
  }
};Client Implementation
Create a simple HTML client to test our serverless WebSocket chat:
<!DOCTYPE html>
<html>
<head>
    <title>Serverless WebSocket Chat</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; }
        #messages { border: 1px solid #ccc; height: 300px; overflow-y: scroll; padding: 10px; margin: 10px 0; }
        input, button { padding: 10px; margin: 5px; }
        .message { margin: 5px 0; padding: 5px; background: #f0f0f0; border-radius: 5px; }
        .own-message { background: #007bff; color: white; text-align: right; }
    </style>
</head>
<body>
    <h1>Serverless WebSocket Chat</h1>
    
    <div>
        <input type="text" id="username" placeholder="Enter username" />
        <button id="connect">Connect</button>
        <button id="disconnect" disabled>Disconnect</button>
    </div>
    
    <div id="messages"></div>
    
    <div>
        <input type="text" id="messageInput" placeholder="Type a message..." disabled />
        <button id="send" disabled>Send</button>
    </div>
    
    <script>
        let ws = null;
        let username = '';
        
        const messagesDiv = document.getElementById('messages');
        const usernameInput = document.getElementById('username');
        const messageInput = document.getElementById('messageInput');
        const connectBtn = document.getElementById('connect');
        const disconnectBtn = document.getElementById('disconnect');
        const sendBtn = document.getElementById('send');
        
        connectBtn.addEventListener('click', connect);
        disconnectBtn.addEventListener('click', disconnect);
        sendBtn.addEventListener('click', sendMessage);
        messageInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') sendMessage();
        });
        
        function connect() {
            username = usernameInput.value.trim();
            if (!username) {
                alert('Please enter a username');
                return;
            }
            
            // Replace with your actual WebSocket API endpoint
            const wsUrl = `wss://your-api-id.execute-api.us-east-1.amazonaws.com/dev?username=${encodeURIComponent(username)}`;
            
            ws = new WebSocket(wsUrl);
            
            ws.onopen = () => {
                addMessage('Connected to serverless chat!', 'system');
                connectBtn.disabled = true;
                disconnectBtn.disabled = false;
                messageInput.disabled = false;
                sendBtn.disabled = false;
                usernameInput.disabled = true;
            };
            
            ws.onmessage = (event) => {
                const data = JSON.parse(event.data);
                if (data.type === 'message') {
                    const msg = data.data;
                    addMessage(`${msg.username}: ${msg.message}`, 'received');
                }
            };
            
            ws.onclose = () => {
                addMessage('Disconnected from chat', 'system');
                resetUI();
            };
            
            ws.onerror = (error) => {
                addMessage(`Connection error: ${error.message}`, 'error');
                resetUI();
            };
        }
        
        function disconnect() {
            if (ws) {
                ws.close();
            }
        }
        
        function sendMessage() {
            const message = messageInput.value.trim();
            if (!message || !ws) return;
            
            ws.send(JSON.stringify({
                action: 'message',
                message: message,
                type: 'chat'
            }));
            
            addMessage(`You: ${message}`, 'sent');
            messageInput.value = '';
        }
        
        function addMessage(text, type) {
            const div = document.createElement('div');
            div.className = `message ${type}`;
            div.textContent = text;
            messagesDiv.appendChild(div);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }
        
        function resetUI() {
            connectBtn.disabled = false;
            disconnectBtn.disabled = true;
            messageInput.disabled = true;
            sendBtn.disabled = true;
            usernameInput.disabled = false;
            ws = null;
        }
    </script>
</body>
</html>Deployment and Testing
Deploy your serverless WebSocket application:
# Deploy to AWS
serverless deploy
# Output will show your WebSocket API endpoint
# Service Information
# service: serverless-websocket-chat
# stage: dev
# region: us-east-1
# stack: serverless-websocket-chat-dev
# api_endpoint: wss://abc123.execute-api.us-east-1.amazonaws.com/devUpdate the WebSocket URL in your HTML client with the endpoint from the deployment output.
Advanced Features and Optimizations
Connection Management
One of the main drawbacks of serverless is cold starts — the delay when a function is invoked after a period of inactivity. To optimize for WebSocket connections:
- Use provisioned concurrency for critical functions
- Implement connection pooling in DynamoDB
- Add TTL (Time To Live) for automatic cleanup
Monitoring and Logging
Use AWS CloudWatch for real-time logging and monitoring by adding structured logging to your Lambda functions:
// Add to each handler
const logger = {
  info: (message, meta = {}) => {
    console.log(JSON.stringify({
      level: 'info',
      message,
      timestamp: new Date().toISOString(),
      ...meta
    }));
  },
  error: (message, error, meta = {}) => {
    console.error(JSON.stringify({
      level: 'error',
      message,
      error: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString(),
      ...meta
    }));
  }
};Cost Analysis and Benefits
With serverless, businesses pay only for the actual execution time of code, with no upfront costs or fixed infrastructure expenses. For a typical chat application:
- API Gateway WebSocket: $1.00 per million connection minutes
- Lambda invocations: $0.20 per million requests
- DynamoDB: Pay-per-request pricing
- SQS: $0.40 per million requests
Compared to running dedicated EC2 instances, serverless can provide 60-80% cost savings for variable workloads.
What’s Next?
In **Part 8**, we’ll explore **Edge Computing with WebSockets** and how to deploy WebSocket applications using Cloudflare Workers and other edge computing platforms for ultra-low latency global applications.
Key topics we’ll cover:
- Edge WebSocket implementation
- Global state synchronization
- Latency optimization techniques
- Performance benchmarking
Questions or feedback? Drop a comment below or connect with me on social media. Don’t forget to subscribe for the next part of our WebSocket architecture series!


 
                                     
                                     
                                     
                                         
                                         
                                        