How Slack Delivers Millions of Scheduled Messages Exactly on Time (Without Crashing)

Imagine telling a personal assistant: "Remind my coworker to inspect the production database at 9:00 AM three weeks from today."

If your assistant writes that note on a random piece of paper and loses it under a coffee mug, your team misses a critical update. Now, multiply that scenario by millions of users scheduling messages across thousands of companies, all expecting delivery down to the exact second.

How do you build a system that guarantees a message scheduled three months ago pops up on a user's screen at the right moment—even if half your server infrastructure crashes the night before?

Let's break down the system architecture behind Slack's scheduled messaging pipeline using simple, real-world concepts.

THE DEEP DIVE (Explained Simply)

1. Vitess & Sharded Primary Storage

What is it?

Vitess is a database management system built on top of MySQL. It acts like an intelligent receptionist standing in front of dozens of standard MySQL databases. Instead of your app needing to know which database holds which company's data, Vitess automatically routes your query to the right place behind the scenes.

Why do we need it?

Slack handles up to 10,000 scheduled messages every single second. A single, traditional database server would instantly choke under this volume of concurrent write operations. Vitess breaks up the massive load by sharding (splitting up) the data across multiple database instances based on workspace_id. To the rest of the application, it still looks and feels like one single database.

What is the catch?

Running sharded databases makes complex cross-workspace queries very difficult. You can't easily join data across two different companies because their records live on physically separate servers.

2. Epoch Timestamp Indexing

What is it?

Instead of saving human-readable date strings like "August 16th, 2026 at 09:00 AM EDT", the database stores schedule execution targets as Epoch Timestamps - a single, plain number representing the exact number of seconds elapsed since January 1, 1970 (e.g., 1770000000).

Why do we need it?

Computers struggle with timezones, daylight saving changes, and localized date strings. Comparing two text strings or converting timezones on the fly burns unnecessary CPU power. Comparing two numbers (1770000000 <= 1770000005) takes fractions of a nanosecond, making time comparisons lightning-fast.

What is the catch?

Epoch timestamps are completely unreadable to human engineers during debugging. You always need helper tools to translate raw timestamps back into readable dates when inspecting logs.

3. The 24-Hour Look-Ahead Hydrator

What is it?

A background service that runs once every hour to look ahead at the main database, locate all messages scheduled to go out within the next 24 hours, and load (hydrate) them into distributed cache.

   Message Scheduled for 50 Days Out
                 │
                 ▼
    ┌─────────────────────────┐
    │   Primary Database DB   │  <-- Sleeps quietly on disk
    └────────────┬────────────┘
                  (24 Hours Before Execution)
                 
    ┌─────────────────────────┐
      Redis Fast Memory RAM    <-- Loaded and ready for dispatch
    └─────────────────────────┘

Why do we need it?

If a user schedules a message 3 months in advance, keeping that payload idling in cache for 90 days wastes money and storage. Keeping it safely stored on disk in the main database costs almost nothing. We only move data into cache right before it is needed.

What is the catch?

If this look-ahead worker crashes or fails silently, upcoming scheduled messages won't get moved into memory on time, causing delivery delays unless a backup mechanism catches them.

4. Redis Sorted Sets (ZSET) as Priority Queues

What is it?

All messages need to be scheduled in next 24 hours need to be kept in cache. But there can be 1000 messages scheduled per second so how to organise those in cache for easy access? A Sorted Set (ZSET) is a unique data structure inside Redis where every item is assigned a numerical score. Redis automatically keeps all items strictly sorted by this score at all times.

In Slack's system:

  • Score: The target target delivery time (Epoch Timestamp).

  • Value: The Message ID (e.g., msg_999).

  • Payload Store: The actual text, user ID, and channel details are stored separately in a Redis HASH key (payload:msg_999).

Why do we need it?

Instead of scanning millions of records to find what needs to be sent right now, workers can ask Redis: "Give me all message IDs where the score is less than or equal to the current timestamp." Because the list is pre-sorted, finding the right messages takes almost zero effort.

What is the catch?

RAM is expensive and volatile. If the Redis server loses power and lacks proper backup snapshots, anything stored exclusively in memory vanishes.

5. Persistent Worker Daemons & Atomic Lua Scripts

What is it?

Instead of traditional cron jobs that spin up once a minute, these are persistent worker programs running constantly inside RAM. Every 250 milliseconds, these workers execute a short, embedded database script called an Atomic Lua Script inside Redis.

-- Simple logic executed inside Redis atomically
local found_ids = redis.call('ZRANGEBYSCORE', 'scheduled_jobs', 0, KEYS[1], 'LIMIT', 0, 100)
if #found_ids > 0 then
    redis.call('ZREM', 'scheduled_jobs', unpack(found_ids))
    return found_ids
end
return {}

Why do we need it?

When you have multiple worker servers pulling jobs off the same queue simultaneously, two workers might grab the same message at the exact same millisecond and send it twice. An atomic Lua script guarantees that fetching the message ID and instantly deleting it from the queue happens as one un-interruptible action. No two workers can ever claim the same job.

What is the catch?

If a Lua script contains a bug or takes too long to run, it completely freezes the Redis instance because Redis runs scripts on a single thread.

6. WebSocket Delivery & Client-Side Idempotency

What is it?

A WebSocket connection is a persistent two-way highway held open between your app and Slack's servers, allowing messages to pop up instantly without refreshing. Client-Side Idempotency means the Slack app on your phone or computer checks every incoming message against its own local database (using SQLite) to ensure it hasn't already processed that specific ID.

[ Worker Engine ] ──(Pushes Message)──► [ User Device ]
                                             │
                                   Checks Local SQLite:
                                  "Already seen msg_999?"
                                      ┌──────┴──────┐
                                     NO            YES
                                      │             │
                                Render to        Silently
                                 Screen           Drop

Why do we need it?

Network connections in the real world drop constantly. If a server sends a message to your phone, but your phone's Wi-Fi cuts out before sending an acknowledgment back, the server might try sending it again. Checking IDs locally on the receiving device ensures you never see duplicate messages on your screen.

What is the catch?

The client device must maintain its own local tracking storage, which increases app memory and battery usage slightly.

7. The 60-Second Sweeper (Orphan Recovery)

What is it?

Assume you have millions of messages in cache and suddenly system crashed. System took 5 mins to recover and once boot up it brought all scheduled messages for next 24 hours in cache. But what about messages that were scheduled during the crash window? Or what if something is missed to be sent from cache? A background service scans the primary database once every 60 seconds looking for orphaned messages - scheduled items whose target delivery time has passed, but are still marked as SCHEDULED instead of SENT.

Why do we need it?

If a Redis server crashes, a network cable gets unplugged, or a worker node dies mid-task, messages can get dropped. The sweeper acts as a fallback system to ensure zero message loss. It scans a safety window from 24 hours ago up to NOW() - 1 minute.

The 1-minute buffer is intentional: it prevents the sweeper from competing with active real-time workers processing live messages right now.

                    SAFETY WINDOW FOR RECOVERY
   ◄───────────────────────────────────────────────────►
   ┌─────────────────────────────────────────┬─────────┐
   │ Past 24 Hours                           │ 1-Min   │  NOW
   │ (Sweeper scans for missed messages)     │ Buffer  │ (Live Workers)
   └─────────────────────────────────────────┴─────────┘

What is the catch?

Querying large database tables for un-sent messages can slow down your database if you don't build specialized database indexes covering both status and scheduled_time.

VISUALS & COMPARISONS

Comparing the System Architecture Tiers

Layer

Technology

Primary Role

Speed / Latency

Data Persistence

Primary Storage

Vitess / MySQL

Source of truth for all schedules

Slow (~15-50 ms)

Permanent (Saved on Disk)

Priority Queue

Redis ZSET

Short-term target index (Next 24 Hours)

Blazing Fast (<1 ms)

Temporary (Held in RAM)

Worker Dispatch

In-Memory Lua Daemons

Atomic job claiming & route processing

Instant (~250 ms Poll)

Ephemeral (Execution State)

Safety Net

DB Sweeper Worker

Recovers orphaned/dropped messages

Batch Run (Every 60s)

Permanent Recovery Check

System Architecture

Ingestion & 24-Hour Hydration Flow

This diagram illustrates how a scheduled message request is accepted, saved to the database, and selectively moved into high-speed memory if it is due soon.

High-Speed Worker Dispatch & Delivery

This diagram shows how workers continuously extract tasks from fast memory and deliver them safely to the client app without duplicate deliveries.

Cheers,

Tech Builder