How Macro Implements Connection Tracking with DynamoDB for Scalable WebSocket Management
Macro uses a dedicated DynamoDB table with TTL-based cleanup and Global Secondary Indexes to durably track active WebSocket connections without maintaining in-process state.
The Connection-Gateway service in the macro-inc/macro repository solves the challenge of tracking millions of ephemeral WebSocket connections across horizontally scaled servers. By persisting connection metadata to DynamoDB rather than memory, the system achieves fault tolerance, automatic cleanup of orphaned records, and efficient user-scoped queries.
The Core Architecture: connection-gateway-table
At the heart of Macro's connection tracking mechanism with DynamoDB lies a single table designed for minimal write latency and automatic expiration. The infrastructure is defined in [infra/stacks/connection-gateway/connection_gateway.ts](https://github.com/macro-inc/macro/blob/main/infra/stacks/connection-gateway/connection_gateway.ts), which provisions the table with two critical features:
- TTL attribute (
expires_at) – Enables DynamoDB to automatically delete stale records without application logic - Global Secondary Index (
ConnectionPkIndex) – Supports reverse lookups from user ID to active connections
The table schema uses a composite key structure optimized for connection lifecycle operations:
| Attribute | Type | Purpose |
|---|---|---|
pk |
String (Partition Key) | Connection identifier (conn#<uuid>) |
sk |
String (Sort Key) | Metadata discriminator (e.g., "metadata") |
expires_at |
Number | Epoch timestamp for TTL-driven cleanup |
user_id |
String (GSI PK) | Enables "all connections for user" queries |
The Connection Lifecycle: Three DynamoDB Operations
1. Connection Open: PutItem with TTL
When a client establishes a WebSocket, the gateway generates a UUID-prefixed connection ID and writes a fresh record with a short TTL window. The ConnectionGatewayClient orchestrates this notification, while the underlying DynamodbClient executes the write.
use uuid::Uuid;
use chrono::Utc;
// Generate unique connection identifier
let connection_id = format!("conn#{}", Uuid::new_v4());
// Calculate 5-minute TTL
let ttl = Utc::now().timestamp() + 300;
dynamo
.client
.put_item()
.table_name(&dynamo.table)
.item("pk", connection_id.clone().into())
.item("sk", "metadata".into())
.item("user_id", user_id.into())
.item("expires_at", ttl.into())
.send()
.await?;
The expires_at attribute is recognized by DynamoDB's TTL feature, which schedules background deletion—no cron jobs or application sweeps required.
2. Heartbeat: UpdateItem to Extend TTL
To keep active connections alive, the gateway periodically refreshes the expires_at timestamp. This UpdateItem operation with an ADD expression on the timestamp effectively implements a "lease renewal" pattern.
The middleware in [crates/macro_middleware/src/tracking/mod.rs](https://github.com/macro-inc/macro/blob/main/crates/macro_middleware/src/tracking/mod.rs) intercepts ping messages and triggers these updates, ensuring that only truly idle connections expire.
3. Connection Close: DeleteItem for Immediate Cleanup
When the WebSocket disconnects gracefully, the gateway issues a synchronous DeleteItem to remove the record instantly. This avoids waiting for TTL and keeps the table size minimal for query performance.
dynamo
.client
.delete_item()
.table_name(&dynamo.table)
.key("pk", connection_id.clone().into())
.key("sk", "metadata".into())
.send()
.await?;
Querying Active Connections: The GSI Advantage
The ConnectionPkIndex Global Secondary Index on user_id enables efficient fan-out operations—critical for features like "disconnect all sessions for this user" or "broadcast to all of user's devices."
let resp = dynamo
.client
.query()
.table_name(&dynamo.table)
.index_name("ConnectionPkIndex")
.key_condition_expression("user_id = :uid")
.expression_attribute_values(":uid", user_id.into())
.send()
.await?;
This query pattern returns all active connection IDs for a given user in a single DynamoDB operation, with consistent performance regardless of total table size.
Implementation Components
| Component | File Path | Responsibility |
|---|---|---|
| Infrastructure (CDK) | [infra/stacks/connection-gateway/connection_gateway.ts](https://github.com/macro-inc/macro/blob/main/infra/stacks/connection-gateway/connection_gateway.ts) |
Defines table, TTL, and GSI configuration |
| Gateway Client | [crates/connection/src/outbound/connection_gateway_client.rs](https://github.com/macro-inc/macro/blob/main/crates/connection/src/outbound/connection_gateway_client.rs) |
High-level interface for connection state changes |
| Request Middleware | [crates/macro_middleware/src/tracking/mod.rs](https://github.com/macro-inc/macro/blob/main/crates/macro_middleware/src/tracking/mod.rs) |
Intercepts WebSocket events and triggers tracking |
| DynamoDB Client | [crates/dynamodb_client/src/client.rs](https://github.com/macro-inc/macro/blob/main/crates/dynamodb_client/src/client.rs) |
SDK wrapper with PutItem, UpdateItem, DeleteItem, Query helpers |
The DynamodbClient abstraction in [client.rs](https://github.com/macro-inc/macro/blob/main/crates/dynamodb_client/src/client.rs) centralizes retry logic, error handling, and table name resolution from environment variables—specifically CONNECTION_GATEWAY_TABLE.
Why DynamoDB for Connection Tracking
Macro's choice of DynamoDB over in-memory stores or Redis delivers three operational advantages:
- Horizontal scalability – Gateway instances remain stateless; any server can handle any connection event by querying the shared table
- Built-in fault tolerance – Connection records survive instance restarts, deployments, or AZ failures
- Automatic garbage collection – TTL eliminates the need for distributed locking or background cleanup jobs for orphaned connections
The trade-off is single-digit millisecond latency for connection operations—acceptable for WebSocket lifecycle management where the data plane (message streaming) remains separate from the control plane (connection tracking).
Summary
- Macro's connection tracking mechanism with DynamoDB centers on the
connection-gateway-tablewith composite keys and TTL-driven cleanup - Connection lifecycle maps to three operations:
PutItem(open),UpdateItem(heartbeat),DeleteItem(close) - The
ConnectionPkIndexGSI enables efficient user-scoped connection queries - TTL on
expires_atautomatically purges orphaned records without application logic - Implementation spans CDK infrastructure, middleware interception, and a thin Rust SDK wrapper in [
crates/dynamodb_client/src/client.rs](https://github.com/macro-inc/macro/blob/main/crates/dynamodb_client/src/client.rs)
Frequently Asked Questions
How does Macro handle abrupt client disconnections without leaving stale records?
The expires_at TTL attribute in [connection_gateway.ts](https://github.com/macro-inc/macro/blob/main/infra/stacks/connection-gateway/connection_gateway.ts) ensures DynamoDB automatically deletes records when the timestamp passes—typically within 48 hours. This compensates for network failures or client crashes that prevent graceful DeleteItem execution.
Can the gateway scale to millions of concurrent connections?
Yes. By storing connection state in DynamoDB rather than instance memory, the ConnectionGatewayClient allows gateway instances to scale horizontally without coordination. DynamoDB handles partition management automatically as table size grows.
Why use conn#<uuid> prefixing for the partition key?
The conn# prefix in [connection_gateway_client.rs](https://github.com/macro-inc/macro/blob/main/crates/connection/src/outbound/connection_gateway_client.rs) enables single-table design patterns—allowing future entity types (e.g., session#, device#) to coexist in the same table with query isolation via key prefixes.
What happens if the UpdateItem heartbeat fails?
A failed heartbeat causes the expires_at timestamp to remain unchanged. The record will eventually TTL-expire and be deleted. The client must reconnect to establish a fresh session, which creates a new PutItem entry with a renewed TTL—an acceptable degradation for connection-oriented protocols.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →