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:

  1. Horizontal scalability – Gateway instances remain stateless; any server can handle any connection event by querying the shared table
  2. Built-in fault tolerance – Connection records survive instance restarts, deployments, or AZ failures
  3. 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-table with composite keys and TTL-driven cleanup
  • Connection lifecycle maps to three operations: PutItem (open), UpdateItem (heartbeat), DeleteItem (close)
  • The ConnectionPkIndex GSI enables efficient user-scoped connection queries
  • TTL on expires_at automatically 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →