Case studies
Chat storage
Chat history is append-heavy and read as recent messages in one channel. Wide-column stores, partitioned by channel and time bucket, fit that pattern. Attachments and search live outside the send path.
Chat history is written constantly, rarely changed, and read as "the latest N messages in channel X." That pattern suits a wide-column store such as Cassandra, ScyllaDB or Bigtable. These systems group rows into partitions and sort each partition by a clustering key. The design question is how to split those partitions so no single one grows too large.
Concept
A partition is the unit a wide-column store places on one node. A clustering key orders rows inside a partition. A time bucket is a fixed period, such as a week, used to split a partition over time.
The big idea
Think of one binder per channel per week. To read the latest messages, you open this week's binder. Older binders sit on the shelf, and you rarely open them. Old binders can be archived or deleted on a schedule.
Without buckets, a busy channel grows one partition forever. Reads and repairs slow down as it grows. Buckets cap the size of each partition.
The analogy breaks for hot channels. Even one week's binder can be overfilled by a very busy room. That case needs its own handling, which the large channels page covers.
Walking through it
The schema has a partition key and a clustering key.
Primary key: (channel_id, time_bucket) clustering: message_id / ts
Payload: sender_id, body, headers, edit_tombstone, ...Time buckets, often weekly, keep partitions manageable. They also let you expire or tier old buckets. Discord's engineering posts describe the hard parts at much larger scale, including hot channels and huge partitions. Treat bucketing and hot-channel splitting as deep-dive topics.
Attachments go to object storage. The message holds a reference, such as a key or a URL. Never put a multi-MB file in a message row.
Search is a separate index, such as Elasticsearch or OpenSearch, fed asynchronously from the write path. The send path must never wait on search. Slack-style products lean hard on search. Consumer messengers with end-to-end encryption often limit it, because the server can't read the bodies.
Deletes and redaction. Include deleted_at or a redacted flag in the schema from day one. Then clients can show "This message was deleted" without breaking the sequence. Deletions run as background jobs that mark rows in the store and the search index.
Common mistakes
- One partition per channel, forever. Busy channels become enormous. Bucket by time.
- Media in message rows. It bloats the store and slows every history read.
- Indexing on the send path. A slow search cluster then slows every message.
- No delete flag. A hard delete can break sequence gaps in client views.
Check yourself
- Why partition by channel and time bucket, not by channel alone? Channels grow without limit. Buckets cap each partition, and old buckets can be expired or tiered.
- Where should search indexing run? Asynchronously, off the send path, so sending never waits on search.
Key takeaways
- Store chat history in a wide-column store, partitioned by channel and time bucket.
- Keep attachments in object storage, with references in the message.
- Index search asynchronously, away from the send path.
- Add a deleted or redacted flag from the start.
Further learning
Discord's post on storing trillions of messages is the best public storage case study for chat.
How Discord Stores Trillions of MessagesEngineer Bo Ingram shares insight into how Discord shoulders its traffic and provides a platform for our users to communicate.discord.com
Next, read about large channels and receipts, which covers fan-out and read tracking at scale.
Related
Case study: chat system
Design a WhatsApp or Slack style messaging system. Connection gateways, per-conversation ordering, wide-column storage and large-channel fan-out are the parts that matter.
Sharding and partitioning
Split data across machines by choosing a shard key, hashing or ranging it, routing requests, handling hot spots, resharding without downtime and generating IDs that stay unique.
Object storage
Store files and media in S3-style object stores. Keep bytes out of your app servers and databases, and design uploads, metadata and costs around that split.