WebSocket
A WebSocket provides a stateful, bidirectional communication channel between clients and the Lambda functions. It is designed for real-time interactions such as multiplayer games, live updates, and event-driven workflows. An Atlas-based application can expose a single WebSocket API composed of multiple channels, each of which can define multiple actions.
WebSocket actions represent the controller layer for real-time communication. Each action receives messages from connected clients, validates input, invokes the necessary services, and optionally sends responses or events back to the client. WebSocket actions do not manage or maintain database sessions and delegate all data operations to the service layer.
Internally, each WebSocket action is implemented as an AWS Lambda handler wrapped by the @action_method decorator, integrated with an API Gateway WebSocket route, with Atlas handling authorization, rate limiting, and connection lifecycle management.
Lifecycle Functions
Atlas supports two special lifecycle actions that are automatically invoked by API Gateway:
$connect
Triggered when a client establishes a new WebSocket connection.
Its responsibilities include:
- Validating authentication token existence (if present)
- Enforcing connection-level security rules
- Registering the connection in connection state tables
- Initializing connection metadata (such as TTLs and token association, if present)
If the $connect action fails, the connection is rejected and never established.
$disconnect
Triggered when a WebSocket connection is closed, either explicitly by the client or implicitly due to expiration, throttling, authorization failure, or network interruption.
Its responsibilities include:
- Cleaning up connection state
- Removing connection records from storage
- Releasing rate-limit or session-related resources
The $disconnect action is best-effort and may not always be invoked in abnormal termination scenarios.
WebSocket Connection State Tables
Atlas uses internal DynamoDB tables to maintain connection state and security guarantees across WebSocket interactions. These tables are automatically created and managed by the framework when WebSocket support is enabled — that is, when at least one channel action is present and enabled.
ws_connections: Stores the active WebSocket connections and their associated metadata.ws_rate_limits: Tracks rate-limiting state for WebSocket traffic.
Initializing the channels package
Before creating actions, create the src/channels directory. This is where all the WebSocket's channels will be stored, each with its own set of actions.
Creating Channels and Actions
To create a channel, follow the steps below:
- Create a nested directory structure inside
src/channelsnamed with the channel's actual name, e.g.src/channels/echo; - Create a Python file inside the channel's directory named with the intended action name, e.g. "ping.py", "pang.py", etc.;
- Inside such file, create a Lambda handler parameterized with the default
eventandcontextand wrapped with @action_method;
Channel and action names must be written in snake_case.
Example:
import json
import boto3
from atlas.decorators import action_method
@action_method
def lambda_handler(event, context):
connection_id = event["requestContext"]["connectionId"]
domain = event["requestContext"]["domainName"]
endpoint_url = f"https://{domain}"
apigw = boto3.client(
"apigatewaymanagementapi",
endpoint_url=endpoint_url
)
message = {
"type": "pong",
"echo": event.get("body")
}
apigw.post_to_connection(ConnectionId=connection_id, Data=json.dumps(message).encode("utf-8"))
return { "statusCode": 200 }
Private Resources
It's possible to create a __resources__ directory inside the channel's directory for storing local, private files used by its actions. The contents in __resources__ need to have names that start with the action's name, e.g. ping_foo_dto.py, pang_bar_dto.py, action_name_foobar.py, etc.
It's also possible to create a directory named after the action's name and store inside it all the files (with arbitrary names) used by such action, e.g. __resources__/ping/any_file.py.
For importing such files into the action's code, you can use sys.path.append as below.
Example:
import json
import boto3
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from .__resources__.ping_foo_dto import PingFooDTO
from atlas.decorators import action_method
@action_method
def lambda_handler(event, context):
connection_id = event["requestContext"]["connectionId"]
domain = event["requestContext"]["domainName"]
endpoint_url = f"https://{domain}"
apigw = boto3.client(
"apigatewaymanagementapi",
endpoint_url=endpoint_url
)
message = PingFooDTO(
type="pong",
echo=event.get("body")
)
apigw.post_to_connection(ConnectionId=connection_id, Data=json.dumps(message).encode("utf-8"))
return { "statusCode": 200 }
Private Configuration
For setting values for action-level API configuration options, create a __config__ directory (inside the channel's directory) for storing local, private JSON files named after their corresponding action's name, e.g. ping.json, pang.json, etc. — one for each action in that channel that needs to have its options configured.
You can get more details on available infrastructure options here.
WSGateway
Atlas provides WSGateway as a unified utility layer for interacting with WebSocket connections.
WSGateway abstracts all environment-specific WebSocket operations—such as sending messages and disconnecting clients—so application code does not need to differentiate between local development and deployed AWS environments.
Whenever an action, service, or infrastructure component needs to communicate with a connected WebSocket client, it should do so through WSGateway rather than interacting directly with API Gateway or local debug internals.
For details on available methods and usage patterns, see the WSGateway documentation.
Example:
from atlas.decorators import action_method
from atlas.ws import WSGateway
@action_method
def lambda_handler(event, context):
# request_ctx is a dict containing "connectionId" and "domainName"
request_ctx = event.get("requestContext", {})
WSGateway.send({"type": "pong", "echo": event.get("body")}, request_ctx)
# or it could also be: WSGateway.send({"type": "pong", "echo": event.get("body")}, {"connectionId": request_ctx.get("connectionId"), "domainName": request_ctx.get("domainName")})
Lifecycle Hooks
Atlas supports WebSocket lifecycle hooks that allow you to execute custom logic when a client connects to or disconnects from the WebSocket API.
These hooks are optional Lambda handler and are implemented as reserved system "actions". They are not callable by clients and are executed automatically by the WebSocket lifecycle. Also, there's no need to add mark them as enabled in api-config.json.
Lifecycle hooks are useful for:
- Logging connection activity
- Initializing or cleaning up server-side state
- Triggering side effects (state mutation, metrics, presence, notifications, etc.)
- Integrating with external systems
system.on_connect
The on_connect hook is executed after a WebSocket connection is successfully established.
To enable it, create a channel named system with a Lambda handler file named on_connect.py.
Example:
# Don't use @action_method, since on_connect is not an action and it's not expected to work as such
def lambda_handler(event, context):
# your custom logic here
return {"statusCode": 200}
This function will be invoked:
- After the connection is registered in
ws_connectionsDynamoDB table - Exactly once per successful $connect event
Connection Snapshot (on_connect)
Before invoking system.on_connect, Atlas injects a connection snapshot into the event payload under the key:
event["connectionSnapshot"]
This snapshot contains the authoritative initial state of the connection exactly as it was persisted in ws_connections (e.g. connection_id, timestamps, authentication data).
The snapshot exists to provide a self-contained and deterministic input for the hook, which is executed asynchronously and must not rely on re-reading mutable state from DynamoDB.
Key characteristics:
- Receives the event and context structure from $connect handler
- Is executed in a fire-and-forget manner (by $connect)
- Cannot block or reject the connection and, since @action_method can't be used with it, it shouldn't be delegated with any authorization logic
- Cannot be triggered manually by the client
- Receives the event and context structure from the
$connecthandler, enriched withconnectionSnapshot
If the file does not exist, the hook is silently ignored.
system.on_disconnect
The on_disconnect hook is executed when a WebSocket connection is closed, regardless of the reason.
To enable it, create a channel named system with a Lambda handler file named on_disconnect.py.
Example:
# Don't use @action_method, since on_disconnect is not an action and it's not expected to work as such
def lambda_handler(event, context):
# your custom logic here
return {"statusCode": 200}
This function will be invoked:
- After the client explicitly disconnects by itself (manually/network failure) or is disconnected by the API (rate limiting exceeded, custom idle timeout logic, etc)
- Exactly once per $disconnect event, before the cleanup logic runs (removal from
ws_connectionsDynamoDB table)
Connection Snapshot (on_disconnect)
Before cleaning up the connection record, Atlas attempts to fetch the last known connection state from ws_connections and injects it into the event payload as:
event["connectionSnapshot"]
Then, after it, system.on_disconnect is called.
This snapshot represents the last authoritative state of the connection at the moment the disconnect was processed.
Because system.on_disconnect is invoked asynchronously and the connection record is deleted as part of the same lifecycle, there is no execution-order guarantee between cleanup and the hook itself.
For this reason:
system.on_disconnectmust not assume the presence of the connection in DynamoDB- All disconnect-related logic should rely exclusively on
connectionSnapshot
Key characteristics:
- Receives the event and context structure from $disconnect handler
- Is executed in a fire-and-forget manner (by $disconnect)
- Best-effort execution (no retries are guaranteed)
- Cannot be triggered manually by the client
- Receives the event and context structure from the
$disconnecthandler, enriched withconnectionSnapshot
If the file does not exist, the hook is silently ignored.