Real-Time WebSocket Architecture Series: Part 7 – Serverless WebSocket Implementation with AWS Lambda

Real-Time WebSocket Architecture Series: Part 7 – Serverless WebSocket Implementation with AWS Lambda

This entry is part 7 of 8 in the series Real-Time WebSocket Architecture Series

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 --> DDB

Architecture Overview

Our serverless WebSocket architecture consists of four main components:

  1. API Gateway WebSocket API – Manages WebSocket connections
  2. Lambda Functions – Handle connect, disconnect, and message events
  3. DynamoDB – Stores connection metadata and application state
  4. 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 uuid

Create 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/dev

Update 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!

Navigate<< Real-Time WebSocket Architecture Series: Part 6 – Production-Grade FeaturesReal-Time WebSocket Architecture Series: Part 8 – Edge Computing WebSockets with Ultra-Low Latency >>

Written by:

373 Posts

View All Posts
Follow Me :
How to whitelist website on AdBlocker?

How to whitelist website on AdBlocker?

  1. 1 Click on the AdBlock Plus icon on the top right corner of your browser
  2. 2 Click on "Enabled on this site" from the AdBlock Plus option
  3. 3 Refresh the page and start browsing the site