advanced~3h

Design a Chat Application on AWS

A worked system-design example for a real-time messaging app on AWS — API Gateway WebSockets for persistent connections, DynamoDB for message history, and the fan-out challenge of delivering a message to potentially many active connections.

Want a visual for this topic?

Generate a diagram tailored to Design a Chat Application on AWS — the AI picks whichever visual (architecture, flowchart, ER diagram, etc.) best fits this specific AWS concept.

Sign in to generate a visual →
0
Subtopics

🎓 Learning objectives

  • Explain why WebSockets (not plain HTTP polling) are needed for real-time chat, and how API Gateway supports them
  • Design a DynamoDB schema for storing and efficiently querying chat message history
  • Explain the connection-tracking problem — knowing which server/connection a given user is currently on
  • Describe how to fan out a message to multiple recipients (group chat) efficiently

What is it?

A chat application's AWS system design centers on API Gateway's WebSocket API type to maintain persistent, bidirectional connections with each connected client, Lambda functions handling connect/disconnect/message events, a DynamoDB table tracking which connection ID(s) belong to which user (for message delivery routing), and a separate DynamoDB table (or the same table with a different key pattern) storing conversation message history, queryable efficiently by conversation and time.

Why it exists

Real-time chat's core requirement — instant, bidirectional message delivery to potentially many simultaneously-connected clients — doesn't fit the traditional request/response HTTP model at all, which is exactly why WebSockets exist as a protocol, and why AWS built a managed WebSocket API type specifically so this pattern doesn't require operating your own persistent-connection-handling server fleet.

Problem it solves

It solves reliably delivering messages in near-real-time to potentially many simultaneously-connected clients, and efficiently storing and retrieving conversation history, without operating custom long-lived connection-handling infrastructure.

Intuition

The two hardest, non-obvious parts of any real-time chat design are (1) how to actually push to a specific open connection from backend code that didn't originate the connection, and (2) how to efficiently query 'the last N messages in this conversation' — API Gateway's Management API directly solves the first, and a conversationId-partitioned, timestamp-sorted DynamoDB table directly solves the second.

Analogy

A plain HTTP-polling chat client is like repeatedly calling someone to ask 'do you have news for me yet' — wasteful and never quite instant. A WebSocket connection is like leaving the phone line open the whole conversation, so either side can speak the instant they have something to say, with no dialing delay.

Technical explanation

API Gateway's Management API endpoint for a WebSocket API is a distinct, separate API (constructed from the API's own endpoint URL with a /@connections/{connectionId} path) that any authorized backend code can call with a PostToConnection request to push arbitrary data to a specific still-open connection — this is what makes it possible for an independent, later Lambda invocation (which didn't handle the original $connect event) to deliver a message to that same client. DynamoDB's Query operation (as opposed to Scan) only reads items matching a specific partition key value, and with a sort key range/limit and ScanIndexForward=false, retrieves the most recent items in that partition in descending time order directly from the index — an O(log n + k) operation for k returned items, rather than an O(n) scan across the whole table.

Architecture

API Gateway's WebSocket API maintains the actual persistent TCP/WebSocket connection with each client and invokes a Lambda function for $connect, $disconnect, and custom message routes, without those Lambda invocations themselves needing to stay running between messages — the connection ID becomes the durable handle used by any later, independent Lambda invocation to route a message back to that same still-open connection via the Management API's PostToConnection call. A connections DynamoDB table (partition key: userId or connectionId, depending on lookup direction needed) tracks live connections, updated on connect/disconnect, while a separate messages table (partition key: conversationId, sort key: a sortable timestamp) stores persisted chat history queryable efficiently per conversation.

Workflow

  1. Client establishes a WebSocket connection to an API Gateway WebSocket API; a $connect route Lambda records the new connectionId against the authenticated userId in a connections DynamoDB table. 2) Client sends a message via a custom route (e.g., sendMessage); the corresponding Lambda persists the message to a messages table (keyed by conversationId + timestamp) and looks up the connectionId(s) of other conversation participants. 3) The Lambda uses API Gateway's Management API PostToConnection call to push the message to each currently-connected recipient. 4) On disconnect, a $disconnect route Lambda removes the connectionId from the connections table. 5) When a client reconnects or opens a conversation, a separate REST endpoint queries recent message history from the messages table.

Example

User A sends a message in a group chat; the message arrives at API Gateway's WebSocket API, is routed to a Lambda function that persists it to the messages table, looks up the connectionId(s) for every other group member from the connections table, and uses API Gateway's Management API to push the message directly to each currently-connected member's open WebSocket connection — offline members simply receive the message the next time they connect and query recent history.

Real-world usage

This WebSocket-API-plus-Lambda-plus-DynamoDB pattern is a common, genuinely production-viable architecture for small-to-medium-scale real-time chat/notification features built on AWS, and is frequently asked as a system-design interview question specifically because it tests whether a candidate understands the connection-tracking and fan-out challenges that don't show up in a simple request/response API design.

Trade-offs

API Gateway WebSockets plus Lambda removes the need to manage long-lived server processes, but each message-send Lambda invocation needs to look up and push to potentially many recipient connections, which for very large groups is better handled by decoupling accept-and-persist from fan-out-delivery via a queue, trading a small amount of delivery latency for a much faster and more scalable sender-facing response. DynamoDB's efficient query pattern for message history requires committing to a specific key design upfront (conversationId partition, time-sortable sort key) — a design that doesn't naturally support other query patterns (like 'all messages a specific user has ever sent across all conversations') without an additional index.

Visual explanation

Picture a hotel switchboard operator (API Gateway WebSocket API) who keeps a card index (the connections table) of which room (connectionId) each guest (user) is currently in. When a call comes in for a guest, the operator looks up the current room on the card index and patches the call straight through — without needing to know or remember anything about how that guest got assigned their room in the first place.

Advantages

  • API Gateway WebSocket API removes the need to run and scale your own long-lived connection-handling server infrastructure

  • DynamoDB's connection-tracking table lets any stateless Lambda invocation deliver a message to a specific user's connection, regardless of which Lambda instance originally handled that connection

  • A conversationId-partitioned, time-sorted DynamoDB schema makes 'load recent messages' a single efficient query rather than a scan

  • Decoupling message fan-out via a queue keeps the sender's perceived latency low and constant, regardless of group size

Disadvantages

  • API Gateway WebSocket connections have a maximum idle timeout and a maximum connection duration, requiring client-side reconnect logic for long-lived sessions

  • Fanning out to very large groups synchronously within one Lambda invocation risks hitting Lambda's execution time limit, requiring the queue-based decoupling pattern for anything beyond small groups

  • DynamoDB's schema decision (conversationId-partitioned) locks in that specific query pattern — supporting a different access pattern later (e.g., full-text search across messages) requires an additional index or a separate search service (like OpenSearch) synced from DynamoDB Streams

  • Presence/typing-indicator features (showing who's currently online or typing) add further real-time state-tracking complexity beyond the core message-delivery design

Common mistakes

  • Trying to implement real-time delivery via client-side HTTP polling instead of WebSockets, adding either unacceptable latency or wasted request volume

  • Storing messages in a table without a query-friendly key design, forcing an expensive scan-and-filter to load a single conversation's recent history

  • Synchronously fanning out to every group member's connection within the sending Lambda invocation for very large groups, risking a timeout instead of decoupling fan-out via a queue

  • Forgetting to clean up the connections table on $disconnect, leaving stale connectionIds that cause failed delivery attempts (and unnecessary API Gateway Management API calls) to connections that no longer exist

In the AWS Console

  1. 1

    API Gateway → Create API → WebSocket API

    Create an API Gateway WebSocket API with `$connect`, `$disconnect`, and a custom `sendMessage` route, each integrated with a Lambda function.

  2. 2

    DynamoDB → Tables → Create table

    Create the connections and messages DynamoDB tables.

  3. 3

    IAM → Roles → [Lambda execution role] → Add permissions

    Grant the message-handling Lambda's execution role permission to call `execute-api:ManageConnections` on the WebSocket API.

🎤 Interview questions

Why use WebSockets instead of having clients repeatedly poll an HTTP endpoint for new messages? (Listen for: WebSockets maintain a persistent, full-duplex connection, letting the server push new messages to the client instantly with no polling delay and no wasted requests when there's nothing new; HTTP polling either introduces latency (polling interval) or wastes resources (very frequent polling), neither of which fits real-time chat's expectations)

How does API Gateway support WebSocket connections, and what does it manage for you? (Listen for: API Gateway's WebSocket API type maintains the persistent connection with the client and invokes a Lambda function on connect, disconnect, and each incoming message (routed by a configurable route key), removing the need to manage long-lived server processes yourself just to hold open connections; API Gateway also provides a @connections management API letting your backend push a message to a specific still-open connection)

How do you know which active WebSocket connection a specific user is currently on, especially with multiple backend Lambda invocations happening independently? (Listen for: maintain a DynamoDB table mapping userId to their current connectionId(s), updated on connect/disconnect — when a message needs to be delivered to a user, look up their connectionId(s) in this table and use API Gateway's Management API PostToConnection call to push to each one; a user connected from multiple devices simply has multiple connectionId rows)

How would you design DynamoDB message storage so a client can efficiently load 'the last 50 messages in this conversation'? (Listen for: a table with conversationId as the partition key and a sortable timestamp (or a composite of timestamp+messageId) as the sort key, letting a Query with ScanIndexForward=false and a Limit efficiently retrieve the most recent N messages for a conversation in one request, without scanning the whole table)

For a group chat with hundreds of members, how would you efficiently fan out one sent message to every member's active connection? (Listen for: rather than the sending client's request handler looping and calling PostToConnection hundreds of times synchronously (slow, and a single Lambda's timeout risk), publish the message once to an SQS queue or SNS topic, with a separate Lambda consumer looking up each member's connectionId(s) and delivering in parallel/batched fashion — decoupling 'accept the message' from 'deliver to everyone' so the sender gets a fast acknowledgment regardless of group size)

💬 Deep Dive with AI

Related concepts

dynamodb-fundamentalssns-eventbridgelambda-serverless

Next Step

Continue to Design a Video Streaming Platform on AWS