# How Macro Implements Connection Tracking with DynamoDB for Scalable WebSocket Management

> Discover how Macro implements connection tracking with DynamoDB for scalable WebSocket management. Learn about TTL-based cleanup and Global Secondary Indexes for durable, state-free connection tracking.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: how-to-guide
- Published: 2026-08-15

---

**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](https://github.com/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)](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`](https://github.com/macro-inc/macro/blob/main/crates/connection/src/outbound/connection_gateway_client.rs) orchestrates this notification, while the underlying `DynamodbClient` executes the write.

```rust
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)](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.

```rust
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."

```rust
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)](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)](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)](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)](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/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)](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/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`](https://github.com/macro-inc/macro/blob/main/crates/connection/src/outbound/connection_gateway_client.rs) 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/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.