API Gateway doesn’t actually manage persistent connection IDs for WebSockets; instead, it assigns a unique connectionId for each individual request that comes through a WebSocket connection.

Let’s see this in action. Imagine a client sending a message to our API Gateway-managed WebSocket.

{
  "action": "sendMessage",
  "data": {
    "message": "Hello from the client!"
  }
}

When API Gateway receives this, it doesn’t just pass the raw JSON along. It wraps it, adding crucial context before sending it to our backend Lambda function. A typical event payload our Lambda might see looks something like this:

{
  "requestContext": {
    "connectionId": "Abc123Def456Ghi789Jkl012Mno345Pqr678Stu901",
    "domainName": "abcdef123.execute-api.us-east-1.amazonaws.com",
    "stage": "prod",
    "routeKey": "sendMessage",
    "eventType": "MESSAGE"
  },
  "body": "{\"action\": \"sendMessage\", \"data\": {\"message\": \"Hello from the client!\"}}",
  "isBase64Encoded": false
}

Notice the connectionId. This isn’t a persistent ID for the entire WebSocket session; it’s tied to this specific message. If the client sends another message, even a millisecond later, API Gateway might generate a new connectionId. This is the core counter-intuitive aspect: API Gateway’s connectionId is ephemeral and request-scoped, not a long-lived session identifier.

So, how do we actually manage WebSocket connections then? We use the connectionId provided in the requestContext to interact with the WebSocket connection via the API Gateway Management API. When our backend receives a message, it uses that connectionId to identify which client sent the message. If we want to send a reply back to that specific client, we use the PostToConnection action of the API Gateway Management API, passing the connectionId it gave us.

For example, if our Lambda function wants to echo the message back to the sender, it would make a call like this (using the AWS SDK):

import boto3
import json

client = boto3.client('apigatewaymanagementapi', endpoint_url="https://abcdef123.execute-api.us-east-1.amazonaws.com") # Use your actual API Gateway endpoint

def lambda_handler(event, context):
    connection_id = event['requestContext']['connectionId']
    body = json.loads(event['body'])

    response_payload = {
        "message": f"Echo: {body['data']['message']}",
        "original_connection_id_seen": connection_id # We're just using this as an example
    }

    client.post_to_connection(
        ConnectionId=connection_id,
        Data=json.dumps(response_payload).encode('utf-8')
    )

    return {'statusCode': 200}

Here, the connectionId from the incoming event is directly used to route the outgoing message back to the originating client. This is how you achieve a form of "session" management: you store the connectionId (along with any other session data you need) in your backend database, keyed by something you control (like a user ID), and then retrieve it later to send messages back.

The connectionId itself is generated by API Gateway and is guaranteed to be unique across all active WebSocket connections for your API. It’s a base64-encoded string, typically around 26 characters long. When a client connects, API Gateway invokes a $connect route, and our backend function for that route receives a connectionId for that new connection. We then typically store this connectionId in a database, associating it with the authenticated user or session. When a client disconnects, a $disconnect route is invoked, and we use the provided connectionId to remove it from our persistent storage.

The crucial takeaway is that the connectionId you receive in a MESSAGE event is specific to that message’s invocation of your backend. If you need to send a message to a client later, you must have already stored that client’s connectionId when they initially connected or sent a previous message. API Gateway doesn’t provide a lookup mechanism to find a connectionId based on, say, a user ID; you have to manage that mapping yourself.

The primary mechanism for managing these ephemeral connectionIds is by storing them in a persistent data store like DynamoDB or Redis. When a client connects, you capture the connectionId from the $connect event and save it. When you need to send a message to that client, you retrieve their stored connectionId from your data store and use the apigatewaymanagementapi to PostToConnection. This allows you to maintain state and send targeted messages to specific clients over their WebSocket connection, even though the connectionId itself is transient per request.

The next challenge you’ll often face is managing the state of these connections, especially when you have multiple instances of your backend service processing messages.

Want structured learning?

Take the full Apigateway course →