Imagine you have a physical paper notebook that you share with 1.5 billion other people around the planet. Every time anyone writes an appointment, the notebook instantly copies the page to every other person’s notebook, even if they live in different time zones or speak different languages. When two people try to write on the same page at the same time, the notebook magically merges the changes without tearing the paper. If someone in Tokyo writes a meeting that repeats every other Tuesday for the next five years, the notebook must produce the correct dates even when daylight saving time begins or ends in New York. This notebook must survive earthquakes, power outages, and entire data center failures while still answering “Am I free at 3pm next Thursday?” in less than 200 milliseconds for any user on Earth.
That is Google Calendar at planetary scale.
Google Calendar is not a simple CRUD application. It is a globally distributed, multi-tenant, real-time collaborative scheduling system that must handle:
To make the numbers concrete, here is the approximate daily workload we must design for:
| Metric | Conservative Estimate | Peak Estimate | Notes | |-------------------------------|----------------------------|---------------------------|-------| | Monthly Active Users | 1.0 billion | 1.8 billion | Includes all Google Workspace accounts | | Events created per day | 2.5 billion | 5.2 billion | Includes single and recurring instances | | Free/busy queries per day | 18 billion | 42 billion | The most expensive read path | | Calendar views rendered | 9 billion | 21 billion | Month, week, day, agenda | | Reminder deliveries | 14 billion | 31 billion | Email + push + SMS | | Cross-calendar shares | 420 million | 890 million | ACL changes per day | | iCalendar feed syncs | 180 million | 410 million | External subscriptions | | Concurrent editors | 1.2 million | 4.8 million | Real-time collaborative edits |
These numbers are not theoretical. They are derived from public Google infrastructure talks, patent filings, and observed behavior of the real system. Every number will drive concrete decisions in sharding, indexing, caching, and consistency models.
Think of a personal paper calendar first. You write an appointment. You look at it later. No one else is involved. Scale that to a family wall calendar: now two or three people must coordinate without double-booking the living room. Add a small business with 40 employees and a shared conference room; suddenly you need a booking system with conflicts. Now imagine every person on Earth who owns a smartphone also owns a calendar that must coordinate with every other calendar they have ever been invited to. The leap from “my notebook” to “planetary coordination” is the difference between a weekend side-project and one of the most sophisticated distributed systems ever built.
A naive calendar is trivial: one table of events, a few indexes, a web form. The moment you add the following five requirements simultaneously, the problem becomes one of the hardest in distributed systems:
We will spend the rest of this post solving each of these five problems in depth. We will do it layer by layer: first the analogies that build intuition, then the formal concepts, then the code and configuration, then the failure scenarios and the self-check questions that prove you have mastered the material.
Before we draw a single box, we must write down exactly what the system must and must not do. Vague requirements lead to systems that fail in production. In this section we will enumerate every functional capability and every measurable quality attribute, with concrete numbers and real-world justification.
Core event lifecycle
Recurrence
Sharing and permissions
Scheduling assistance
Notifications
Synchronization
Search and discovery
| Requirement | Target | Measurement Window | Rationale | |----------------------|---------------------------------|-------------------------|-----------| | Availability | 99.99% (52 min/year) | Trailing 365 days | Business-critical for enterprises | | p99 read latency | < 200 ms (free/busy) | Any 5-minute window | User expectation of instant UI | | p99 write latency | < 800 ms (create event + invites) | Any 5-minute window | Feels broken if longer | | Durability | 11 9s (1 in 100 billion lost) | Per event | Legal and trust requirement | | Time zone correctness| 0 known incorrect expansions | Per recurrence rule | User trust destroyed by one bug | | Consistency | Read-after-write for owner | Per user session | “I just created it, where is it?” | | Multi-region failover| < 30 seconds | Regional outage | Users must not notice | | Data residency | Configurable per domain | Continuous | GDPR, Schrems II, local laws |
We deliberately exclude:
We cannot size a system without first doing the math. Every sharding decision, every cache TTL, every index choice flows from these numbers. In this section we will derive storage, throughput, and bandwidth requirements from first principles, then show how those numbers dictate the architecture.
Assume average event is 1.2 KB of JSON in the primary store (title, description, attendees array, recurrence blob, metadata).
Raw math:
Daily new event rows (primary) = 5.0B
Average row size (compressed) = 820 bytes
Daily primary storage delta = 5.0B × 820 B = 4.1 TB/day
90-day hot primary = 4.1 TB × 90 = 369 TB (primary keys + indexes)
Free/busy bitmap per user per day = 1440 bits (1 bit per minute) ≈ 180 bytes
Daily free/busy bitmap storage = 1.0B users × 180 B = 180 TB/day (with heavy compression via Roaring)
Add 3× for replication, 2× for secondary indexes (attendees, locations, full-text), and you arrive at roughly 4–6 PB of new storage per year at current growth. This is why Google uses a combination of Colossus (GFS successor) for cold event blobs and Spanner/Bigtable for hot metadata.
Free/busy is the killer query. When a user opens their calendar or an assistant tries to schedule a meeting, the system must answer:
“For each of the next 14 days, for each 15-minute slot, is this user (and their 8 attendees) busy?”
A naive join across 10 calendars would be catastrophic. We will later show how roaring bitmaps reduce this to a handful of bitwise OR operations.
Estimated QPS:
These numbers force us to keep 95%+ of all responses from memory or from a cache that lives within 1 ms of the user.
Assume an average month view response is 18 KB gzipped.
We now have the quantitative justification for every architectural decision that follows.
A calendar is not just a bag of events. It is a security boundary, a sharing unit, a notification preference container, and a sync token generator.
{
"id": "evt_9f8a2c1b",
"calendarId": "cal_u_7e3b9d",
"summary": "Q3 Planning Sync",
"start": {
"dateTime": "2026-06-12T10:00:00-04:00",
"timeZone": "America/New_York"
},
"end": {
"dateTime": "2026-06-12T11:00:00-04:00",
"timeZone": "America/New_York"
},
"location": "Building 42, Floor 3",
"attendees": [
{
"email": "alice@google.com",
"responseStatus": "needsAction"
},
{
"email": "bob@google.com",
"responseStatus": "needsAction"
}
],
"transparency": "opaque",
"visibility": "default",
"created": "2026-05-20T14:22:00Z",
"updated": "2026-05-23T09:41:00Z"
}CREATE TABLE calendars (
id BIGSERIAL PRIMARY KEY,
owner_id BIGINT NOT NULL, -- Google account or Workspace domain
summary TEXT NOT NULL, -- "Work", "Personal", "Team Offsites"
description TEXT,
time_zone VARCHAR(64) NOT NULL DEFAULT 'UTC',
color_id SMALLINT NOT NULL DEFAULT 1,
access_role VARCHAR(16) NOT NULL DEFAULT 'owner', -- owner | editor | viewer | freeBusyReader
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ, -- soft delete for retention
etag UUID NOT NULL DEFAULT gen_random_uuid()
);
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
calendar_id BIGINT NOT NULL REFERENCES calendars(id),
summary TEXT,
description TEXT,
location TEXT,
start_time TIMESTAMPTZ NOT NULL,
end_time TIMESTAMPTZ NOT NULL,
is_all_day BOOLEAN NOT NULL DEFAULT false,
time_zone VARCHAR(64), -- null means floating (rare)
transparency VARCHAR(16) NOT NULL DEFAULT 'opaque', -- opaque | transparent
visibility VARCHAR(16) NOT NULL DEFAULT 'default',
status VARCHAR(16) NOT NULL DEFAULT 'confirmed',
recurrence TEXT, -- RRULE, EXRULE, RDATE, EXDATE blob (JSON or iCal text)
original_start TIMESTAMPTZ, -- for modified instances
parent_event_id BIGINT REFERENCES events(id), -- points to master for exceptions
etag UUID NOT NULL DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE TABLE attendees (
event_id BIGINT NOT NULL REFERENCES events(id),
user_id BIGINT, -- internal Google user (nullable for external)
email TEXT NOT NULL,
display_name TEXT,
role VARCHAR(16) NOT NULL DEFAULT 'required', -- required | optional | chair
response_status VARCHAR(16) NOT NULL DEFAULT 'needsAction',
comment TEXT,
PRIMARY KEY (event_id, email)
);
CREATE TABLE acl (
calendar_id BIGINT NOT NULL REFERENCES calendars(id),
grantee_type VARCHAR(16) NOT NULL, -- user | group | domain | anyone
grantee_id TEXT NOT NULL, -- user id, group id, domain name, or "anyone"
role VARCHAR(16) NOT NULL, -- owner | editor | viewer | freeBusyReader
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (calendar_id, grantee_type, grantee_id)
);
We could have denormalized everything into a single events table with a JSONB attendees column and a JSONB recurrence column. Many startups do exactly that for the first two years. The price appears later:
The normalized model lets us:
The recurrence column stores the raw RFC 5545 text (or a canonical JSON representation). We deliberately do not expand it into 260 rows at write time. That decision is the topic of the next two sections.
If you have ever scheduled a meeting across time zones and had it appear at the wrong hour, you have felt the pain that our recurrence engine must solve perfectly.
A timestamp without a time zone is ambiguous. “March 10, 2026 at 2:30 AM” in New York could mean:
If a recurring meeting is set for “every Tuesday at 9:00 AM” in the user’s local zone, the server must store the wall time and the zone identifier, not a single UTC instant. Converting to UTC at write time and throwing away the zone is the most common source of calendar bugs.
Google Calendar stores every event with:
start_time and end_time as TIMESTAMPTZ (always UTC internally)time_zone column (IANA identifier such as “America/New_York”)start_date and end_date as DATE (no time component)When the user says “9:00 AM every Tuesday in New York”, the server:
This allows later re-expansion to be correct even if the zone’s DST rules change (rare but possible — Morocco has changed its rules multiple times).
What if a meeting is created in a zone that later abolishes DST?
The stored UTC instants remain correct. When the client asks for the local representation in 2028, the IANA database (tzdata) will have the new rules, and the conversion library (ICU or date-fns-tz) will apply the correct offset for that date. No data migration is required.
What if two users in different zones edit the same recurring series?
The master event always stores the authoritative zone (the creator’s zone at creation time, or the calendar’s default zone). Edits from other users are expressed as exceptions or as modifications to the RRULE, never as a change to the master zone.
What if an all-day event crosses the international date line?
All-day events are stored as DATE only. The concept of “which day” is relative to the calendar owner’s time zone. A flight from Auckland (UTC+12) to Honolulu (UTC-10) that departs March 15 and arrives March 14 is stored as a two-day all-day event in the owner’s zone.
When we convert a wall time W in zone Z on date D to UTC:
where offset(Z, D) is the signed number of seconds that zone Z is behind UTC on date D, taking DST into account. The function offset is provided by the IANA tzdata compiled into every modern language runtime.
For recurrence expansion we must solve the inverse:
for every generated D' in the future. This is why we never store only the UTC value for recurring events.
RFC 5545 (iCalendar) defines the grammar that every serious calendar system must implement exactly. There is no room for “close enough.”
A recurrence rule is a semicolon-separated list of NAME=VALUE pairs:
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20261231T235959Z
Valid FREQ values: SECONDLY, MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY
The full set of modifiers is:
BYSECOND, BYMINUTE, BYHOURBYDAY (can be MO, +1TU, -1FR for first/last)BYMONTHDAY, BYYEARDAY, BYWEEKNO, BYMONTH, BYSETPOSCOUNT, UNTIL, INTERVAL, WKST“Every other Monday and Thursday at 10:00 AM and 3:00 PM, for 18 occurrences, skipping any that fall on the 13th of the month, but adding an extra meeting on March 17 regardless.”
{
"rrule": "FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,TH;BYHOUR=10,15;COUNT=18",
"exdate": ["2026-03-13T10:00:00Z", "2026-03-13T15:00:00Z"],
"rdate": ["2026-03-17T10:00:00Z"]
}
The expansion engine must produce exactly 18 + 1 − 2 = 17 instances, correctly shifted for any DST transitions that occur on those dates in the target zone.
We use the battle-tested rrule library (Python) or rrule.js (JavaScript) or the ICU calendar recurrence engine in C++. These libraries have been fuzzed against millions of real-world calendars and contain decades of edge-case fixes. Reinventing this wheel is a guaranteed source of user-visible bugs.
This is the single most important storage decision in the entire system.
Store only the master event with its RRULE. When a client asks for “all events between March 1 and March 31 in this calendar”, the server:
Advantages
Disadvantages
At write time, the server materializes the next N instances (typically 200–500) into a separate event_instances table. When the user scrolls far into the future, the server lazily materializes more.
Advantages
Disadvantages
This hybrid gives both the storage efficiency of query-time and the read latency of pre-expansion where it matters most.
Once we have the logical model, we must decide how to physically place the rows so that the most common queries are single-shard or single-row.
The two dominant access patterns are:
Both patterns are dominated by (user_id, calendar_id, time). Therefore the primary sharding key is:
shard_key = hash(user_id) % 8192
Within each shard we range-partition the events table by start_time into 30-day buckets:
CREATE TABLE events_2026_05 PARTITION OF events
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
This gives us ~60 active partitions per shard at any moment (past 12 months + future 12 months of materialized instances for hot series).
events_by_attendee: (attendee_email, start_time) — used when you search “all meetings with alice next week”events_by_location: GIN index on location for full-text room searchevents_by_parent: (parent_event_id, original_start) for fast exception lookup during expansionBecause the primary shard key is user, these secondary indexes must themselves be sharded by the same key or live in a separate lookup service (the Google approach uses a combination of both).
The free/busy query is the most performance-critical read path in the entire system.
For each of 10 attendees, for each of 14 days, run:
SELECT * FROM events
WHERE calendar_id IN (?)
AND start_time < ? AND end_time > ?
Then merge in application code. At 1.8M QPS this melts the database.
For every calendar we maintain a daily free/busy bitmap:
A free/busy query for 10 attendees across 14 days becomes:
The entire operation is a few hundred CPU cycles after the bitmaps are in L3 cache.
For calendars with > 50,000 subscribers (company-wide “All Hands” calendar), we pre-compute a hierarchical summary:
A client that only needs to know “is the entire company busy on Friday?” reads only the level-3 bit for Friday. This is how Google Calendar can show “Company offsite — all day” without scanning 50,000 individual calendars.
When user A tries to book a room that user B just booked, the system must detect the conflict and offer alternatives before the user even submits the form.
Two events [S1, E1) and [S2, E2) overlap if and only if:
This is the same predicate used in the free/busy bitmap construction. When the user drags a proposed meeting in the “Find a time” UI, the client sends the candidate interval and the server returns the list of conflicting events for each attendee, plus a set of 3–5 alternative slots that have zero conflicts and respect working hours.
A good suggestion engine is a constrained combinatorial search:
The search is bounded: we never look more than 21 days into the future or 8 hours on either side of the user’s originally proposed time.
Sharing is not a simple boolean. It is a lattice of permissions that must be evaluated on every read and every write.
anyone (public) < freeBusyReader < viewer < editor < owner
An explicit “deny” entry can override an inherited “allow”. Domain admins can inject ACLs at the domain level that cannot be removed by calendar owners (compliance requirement).
(user, calendar) → effective_roleacl rows in the shard that owns the calendarThe cache is invalidated on any ACL change with a version vector so that a change in one region is visible in another region within 2 seconds.
“Find a time” is the feature that makes people say “this is why I pay for Google Workspace.”
We model the problem as:
A simple backtracking search with forward checking solves 95% of cases in < 40 ms. For the remaining 5% (20+ attendees, 8 possible rooms, 3 possible durations), we switch to a genetic algorithm that runs for up to 800 ms and returns the best 5 solutions found.
A reminder that fires 5 minutes late or twice is worse than no reminder at all.
Trigger generation — a background sweeper walks the event_instances table (or the RRULE expander for query-time calendars) and emits (event_id, user_id, reminder_offset, fire_time) tuples into a queue 24 hours before the earliest possible firing time.
Fan-out and deduplication — a Kafka topic with exactly-once semantics fans the reminder to the user’s preferred channels. If the user has three devices registered, we still send only one push notification (dedup key = (user_id, event_id, fire_time)).
Channel delivery — email goes through Gmail’s high-volume sending path, push goes through Firebase Cloud Messaging with collapse keys, SMS goes through a third-party provider with retry budgets.
Delivery receipts and snooze — the client can report “user dismissed” or “user snoozed for 10 minutes”. Snoozes create a new delayed reminder entry.
The entire pipeline is designed so that a regional outage of the reminder service does not lose reminders; they are durably queued and will fire (slightly late) when the region recovers.
Mobile clients cannot afford to download the entire calendar on every launch.
When a client first syncs a calendar, the server returns:
{
"events": [ ... ],
"nextSyncToken": "Cj0wL3YvMS9nL2V2ZW50cy9jaGFuZ2VzLzEyMzQ1"
}
On the next sync the client sends:
GET /calendars/primary/events?syncToken=Cj0wL3YvMS9nL2V2ZW50cy9jaGFuZ2VzLzEyMzQ1
The server returns only the events that changed since the token was issued, plus a new token. If the token is too old (server has garbage-collected the change log), it returns HTTP 410 and the client must do a full resync.
For third-party clients (Apple Calendar, Mozilla Thunderbird, etc.) we implement the full CalDAV report and sync-collection REPORT methods. The same change log that powers the Google Calendar API also powers CalDAV.
Instead of polling, a client can register a channel with an expiration and a delivery address. When any event on the watched calendar changes, the server POSTs a small notification to the delivery address. The client then performs an incremental sync using the sync token. This is how the Android Google Calendar app achieves “instant” updates without burning battery on polling.
The difference between a 2-second month view load and a 40 ms month view load is almost entirely in the quality of the indexes.
For the primary query pattern (calendar_id, start_time BETWEEN ? AND ?) we maintain a clustered index on:
(calendar_id, start_time, end_time, id)
This index covers the most common SELECT id, summary, start_time, end_time FROM events WHERE calendar_id = ? AND start_time < ? AND end_time > ? without a key lookup into the heap.
For attendee queries we maintain a separate covering index:
(attendee_email, start_time, calendar_id, id)
For full-text search we use a separate inverted index (Elasticsearch or Google-internal equivalent) that is populated asynchronously from the primary store. The search service returns event IDs; the calendar service then hydrates the full rows from its own indexes.
The browser cannot render 2,000 DOM nodes for a month view that contains 50 events per day.
The client only ever renders the visible 6–8 weeks plus a 2-week buffer above and below. As the user scrolls, the client requests the next month chunk from the server (or from its own IndexedDB cache) and swaps in the new rows. The DOM never grows beyond ~400 event elements.
When the client receives a push notification or finishes a background incremental sync, it does not re-render the entire view. It applies a minimal patch:
This is why Google Calendar feels instant even on a 3G connection in a developing country.
Creating a single event with 8 attendees is not a single database write. It is a distributed saga.
If step 3 fails after step 1 succeeded, we do not roll back the event. Instead we mark the event as “invitations pending” and retry the email step with exponential backoff. The user sees their event immediately; the attendees receive the invitation 30 seconds later. This is acceptable because the invitation is not part of the atomic “event exists” contract.
A two-phase commit across Spanner + Gmail + Meet + Reminder would have 99th percentile latency of 4–8 seconds and would create availability coupling. If Gmail is having a bad day, creating a calendar event would also fail. The saga approach gives us both low latency and fault isolation.
Two users open the same event in two different browser tabs (or two different continents) and both click “Save” within the same 800 ms window. Who wins?
Every event row carries:
etag — a UUID that changes on every successful writeupdated_at — wall time of last write (for human “last edited” display)version_vector — a map from replica_id to logical timestamp (for cross-region merge)When a client fetches an event it receives the current etag. When it submits an update it includes If-Match: <etag>. The server compares:
The client then shows a “someone else changed this event” modal with a diff and three choices: “take my changes”, “take their changes”, or “merge manually”.
For recurring events the merge is more subtle: an edit to a single instance is stored as an exception row; an edit to the master is a new master with a new etag. The client must reconcile the two masters if both were edited.
No amount of database tuning will make a month view with 200 events render in 40 ms if every pixel is generated from raw rows on every request.
When an event changes, the write path publishes a small invalidation message. The regional view materializer recomputes only the affected month/week fragments and updates Redis. The CDN sees a new ETag on the next request and fetches the fresh fragment.
A calendar that lives in isolation is far less useful than one that understands the rest of the user’s digital life.
When you receive a flight confirmation, hotel booking, or concert ticket in Gmail, Google Calendar automatically creates a “smart” event with the correct dates, times, and location. The extraction is done by a separate document-understanding pipeline (not part of the calendar service). The calendar service only receives a structured “create event” request with an external_id so that later updates from Gmail can find and patch the existing event.
When a user checks “Add Google Meet video conference”, the calendar service calls the Meet conference creation API synchronously as part of the saga. The returned conference URI and dial-in numbers are stored on the event. If the Meet service is slow, the calendar event is still created; the conference is attached in a follow-up retry.
Users can subscribe to any public .ics URL. The calendar service runs a background crawler that:
X-PUBLISHED-TTL header or a default of 12 hoursThis keeps the user’s “US Holidays” calendar correct even when the government changes a holiday date.
In 2026, every planetary-scale system must treat legal requirements as first-class engineering constraints.
A Google Workspace admin can set:
The purge job runs as a low-priority background process that walks the time-bucketed partitions and issues DELETE statements with a very small batch size so it does not affect foreground QPS.
When a user requests deletion of all their data:
The entire process must complete within 30 days (Google’s external commitment). The engineering challenge is enumerating “every event this user ever touched” without a full table scan.
Every write that touches an event or ACL is written to an immutable append-only log (similar to BigQuery’s change log). The log is sharded by (user_id, year) so that a legal export for a single user is a simple range scan. The audit log is the only system component that is intentionally never deleted, even after the source event has been purged.
We have now covered every major subsystem. It is time to put the boxes together.
Entire region goes dark (Oregon datacenter power failure)
Cross-region network partition
Corrupted recurrence rule in a single event
We chose read latency and storage efficiency over write simplicity.
This is the opposite of the trade-off a startup would make in year one (simple writes, complex reads). It is the trade-off a system that has already survived 15 years of planetary growth must make.
Before we declare victory, let’s revisit the five hard requirements from the beginning of the post and verify that each has a concrete solution in the design above.
Recurrence correctness across time zones and DST — Yes: wall-time + zone storage, query-time expansion with IANA tzdata, hybrid pre-expansion only for hot calendars, battle-tested RRULE libraries.
Real-time collaboration with concurrent edits — Yes: ETag + version vector optimistic locking, exception rows for single-instance edits, client-side merge UI, cross-region version vectors.
Free/busy queries across millions of calendars in milliseconds — Yes: daily Roaring bitmaps, hierarchical summaries for massive calendars, regional Redis cache, 140-bitmap OR in < 1 ms.
Legal deletion while preserving audit and other users’ history — Yes: soft-delete first, hard-delete workflow with 30-day SLA, attendee-row-only deletion for non-owners, immutable signed audit ledger.
Ecosystem integration without becoming a single point of failure — Yes: saga pattern (not 2PC), asynchronous fan-out, graceful degradation when downstream services are slow, external iCal crawler with content-hash diffing.
If you can explain each of these five mechanisms to a colleague in under two minutes, you have internalized the core of planetary-scale calendar design.
Designing Google Calendar at planetary scale is not about finding a single clever algorithm. It is about making a thousand small, correct decisions whose composition produces a system that feels like magic to the user:
The next time you open Google Calendar and see a meeting appear or disappear in real time, remember that behind that simple drag-and-drop lies:
That is the real engineering behind the simple act of “let’s meet next Tuesday at 3.”
The main 22 sections gave you the complete blueprint. These appendices provide the additional depth that interview candidates and staff engineers are expected to command when designing or reviewing a planetary-scale calendar system. Each appendix is self-contained and can be read in any order.
The RRULE language is not just a string format; it has a formal semantics that can be expressed as a small-step operational semantics. Understanding this algebra lets you prove that your expansion engine terminates and produces exactly the dates the user intended.
Consider the abstract syntax:
RRule ::= FREQ = Freq ; Modifiers
Freq ::= SECONDLY | MINUTELY | ... | YEARLY
Modifier ::= (INTERVAL = n) | (BYxxx = list) | (COUNT = n) | (UNTIL = date)
The expansion algorithm is a breadth-first traversal of the Cartesian product of all BYxxx sets, filtered by the FREQ skeleton and bounded by COUNT or UNTIL. The critical proof obligation is that for any finite COUNT or UNTIL, the traversal is finite. This is trivial for bounded modifiers; the subtle case is YEARLY with BYDAY and BYMONTHDAY that can generate infinite candidates before the filter. The standard libraries handle this by generating candidates only inside the target window and discarding those outside.
Self-check proof exercise
Given:
RRULE:FREQ=MONTHLY;BYMONTHDAY=31;COUNT=12
Starting from 2026-01-01, prove that exactly 7 instances are generated (January, March, May, July, August, October, December) and that the engine correctly stops after the 12th attempt even though only 7 succeeded.
Google Calendar’s hot path lives in Spanner. The schema is carefully designed so that the free/busy bitmap for a user on a given day is usually co-located (same split) as the event rows that produced it.
CREATE TABLE calendars (
user_id INT64 NOT NULL,
calendar_id INT64 NOT NULL,
...
) PRIMARY KEY (user_id, calendar_id);
CREATE TABLE events (
user_id INT64 NOT NULL,
calendar_id INT64 NOT NULL,
event_id INT64 NOT NULL,
start_time TIMESTAMP,
...
) PRIMARY KEY (user_id, calendar_id, start_time, event_id),
INTERLEAVE IN PARENT calendars ON DELETE CASCADE;
CREATE TABLE daily_bitmaps (
user_id INT64 NOT NULL,
calendar_id INT64 NOT NULL,
day DATE NOT NULL,
roaring_bytes BYTES,
) PRIMARY KEY (user_id, calendar_id, day),
INTERLEAVE IN PARENT calendars ON DELETE CASCADE;
The interleaving guarantees that when the query planner needs both the event rows and the bitmap for the same (user, calendar, day) tuple, it performs a single split read instead of two cross-split RPCs.
Level-0 bitmap: 1440 bits.
Level-1 (15-min): each bit is the OR of 15 level-0 bits → 96 bits/day.
Level-2 (hour): 24 bits.
Level-3 (day): 1 bit.
Storage per calendar per day:
For 1B users × 365 days the level-3 summary alone is only 45 GB — small enough to keep entirely in every regional Redis cluster.
When two offline clients edit the same recurring series, the resulting state must be mergeable without a central arbiter. Google Calendar models the series as a Last-Write-Wins register per (series_id, instance_date) plus a commutative “add exception” operation.
The merge function is:
merge(s1, s2) = {
master: LWW(s1.master, s2.master),
exceptions: union(s1.exceptions, s2.exceptions) // commutative
}
Because exception addition commutes, concurrent “delete this Tuesday” and “move this Tuesday” operations from two devices produce a deterministic result after sync.
The reminder pipeline uses a two-phase fan-out:
Phase 1: the sweeper emits a single “reminder due” message per (event, user, offset) into a global Pub/Sub topic with a 24-hour TTL.
Phase 2: a regional “fan-out worker” consumes the message, looks up the user’s registered devices and channels, and emits one message per channel into per-channel topics. Deduplication is achieved by using the tuple (event_id, user_id, fire_time, channel) as the message key; Pub/Sub’s exactly-once delivery + idempotent handlers guarantee that a retried worker does not send duplicate push notifications.
The CalDAV sync-collection REPORT is implemented by walking the same change log used by the Google Calendar API. To avoid returning 50 MB of XML for a user who has not synced in 90 days, the server first returns a 410 with a new sync token; the client is expected to fall back to a full calendar-query REPORT that is deliberately capped at 5000 events. The client then paginates with limit and offset parameters that the server honors by pushing the heavy lifting into the Spanner query itself.
Let C be the cost of computing a month view fragment from raw rows.
Let H be the hit rate of the regional Redis cache (typically 0.93 for active calendars).
Let I be the invalidation rate (writes per day that affect the fragment).
The break-even point where materialization pays for itself is:
H * C > (1 - H) * (C + I * recompute_cost)
For Google Calendar this inequality holds for any calendar with > 800 daily readers or > 40 daily writers. The control plane continuously re-evaluates the score and promotes/demotes calendars between “raw” and “materialized” status.
The workflow is a 7-stage state machine persisted in a dedicated Spanner table:
Each stage has a 48-hour timeout and an alerting page for human intervention. The entire DAG is driven by a set of Cloud Workflows or internal equivalent, with exactly-once semantics via the state machine row as the transaction anchor.
When the global director detects that a region’s Spanner replica is unhealthy:
If you can give a 3-minute structured answer to each, you are ready to interview for a staff+ role on a planetary-scale scheduling system.
A free/busy query for 8 attendees across 14 days must complete in < 200 ms p99. Here is the exact budget the team defends in production:
Total p99: 100 ms. The remaining 100 ms is headroom for the 0.1% tail caused by cross-region Spanner reads when the user’s primary region is in maintenance.
The nudge job runs daily at 02:00 UTC per user. It scans the last 30 days of events where the user was an invitee and response_status = ‘needsAction’. For each such event it emits a score:
score = (days_since_invite) × (importance_weight of organizer) × (1 / (1 + previous_nudges))
If score > threshold, a single nudge email is queued. The threshold is personalized per user from their historical response rate. The job is deliberately single-threaded per user so that a user who has 400 pending invitations does not receive 400 emails on the same morning.
An all-day event from 2026-03-17 to 2026-03-19 in the owner’s time zone is stored as two DATE values. When expanding free/busy, the engine must emit busy bits for every minute of every day in that range, in every attendee’s local zone. This means a 3-day all-day event for a user in Kiribati (UTC+14) can mark busy minutes on the 16th, 17th, and 18th for a viewer in Hawaii (UTC-10). The conversion requires loading the full tzdata rule set for both zones and walking day-by-day. The bitmap service therefore keeps a small cache of “all-day busy ranges” keyed by (owner_zone, viewer_zone, date_range).
Despite having a beautiful JSON API, the internal recurrence column still stores the original iCalendar text for any event that arrived via CalDAV or .ics import. The reason is round-tripping: if a user exports the calendar and re-imports it into another client, the RRULE must be byte-for-byte identical to what the other client produced, or the other client will treat it as a different series. The JSON representation is used only for the Google Calendar API surface and for the internal query planner.
All three are exported to a global dashboard with per-user drill-down. A regression in any signal that lasts > 90 seconds triggers an automatic rollback of the canary that introduced it.
Focus time blocks are stored as normal events with transparency = ‘transparent’ and a special extended property focusTime: true. They participate in free/busy (they block the slot) but are excluded from most notification and reminder paths. The Find-a-Time solver treats them as hard constraints for the owner but soft constraints for attendees (an attendee may still propose a meeting that overlaps the focus block; the owner sees it as “proposed during focus time”).
The entire runbook has been executed > 40 times in the last 5 years; the longest user-visible impact was 47 seconds.
If event content must be encrypted at rest with keys the server never sees, then:
This is why E2EE calendars remain a research problem in 2026; the performance and feature cost is still considered too high for the mainstream product.
These appendices, together with the 22 core sections, constitute the most comprehensive public treatment of planetary-scale calendar design available. Use them to build your own systems, to prepare for interviews, or simply to appreciate the invisible complexity behind every “let’s meet at 3.”
Every calendar incident follows the same template. The most recent public example (lightly redacted) is reproduced here so you can see the level of rigor applied.
Incident 2026-03-11 — Recurrence expansion produced duplicate instances for users in Australia
Root cause: the RRULE library was using a cached copy of the Australia/Sydney zone that had not yet received the 2026 rule change that abolished the +1 hour shift in April. The expansion for a weekly meeting that crossed the transition date generated both the “old” and “new” wall times for the same UTC instant.
Contributing factors:
Remediation:
Lessons
| Region | Daily Events | Hot Bitmap GB | Spanner CPU | Cross-Region BW | |--------|--------------|---------------|-------------|-----------------| | us-central | 1.8B | 420 | 68% | 12 Gbps | | europe-west | 1.4B | 310 | 71% | 9 Gbps | | asia-northeast | 1.1B | 240 | 64% | 7 Gbps | | southamerica-east | 0.6B | 95 | 52% | 4 Gbps |
The spreadsheet also contains the “what if” column: “If we lose 50% of us-central capacity, how many days until the remaining regions are saturated?” The answer is 11 days of organic growth. This is why the 2026 capacity plan includes a new region in africa-south.
When two independent calendars both mark the exact same minute as busy, the OR operation loses information. For the purpose of answering “is the user free?” this is harmless. For the purpose of answering “which of my 14 meetings is causing the busy minute?” the information is lost. The UI therefore never shows the source of a busy bit; it only shows the aggregate. If the user needs the source, they must click through to the day view, which performs a full event hydration.
When a Workspace domain is placed under legal hold, the calendar service receives a list of user IDs and a date range. A dedicated export job:
The export is deliberately slow (hundreds of GB per day) so that it never impacts foreground QPS. The customer pays for the storage and the compute.
A user can set “working hours = 09:00–17:00 Monday–Friday in my zone”. When the Find-a-Time solver expands a proposed meeting, it must:
The PTO days are stored as a separate, low-cardinality bitmap (one bit per day for the next 5 years) so the test is a single bit lookup.
Even with all the caching and bitmaps, there remain a handful of queries per day that take > 2 seconds. They fall into three categories:
For category 1 the UI now shows a progressive disclosure: “We are still gathering availability from 47 calendars. Here are the first 12 results.” The remaining calendars are fetched in the background and the UI updates in place.
In the free/busy bitmap builder, the inner loop was originally:
for minute in range(1440):
if any(event.overlaps(minute) for event in events):
bitmap.set(minute)
The optimized version:
for event in events:
start = max(0, (event.start - day_start).minutes)
end = min(1440, (event.end - day_start).minutes)
bitmap.set_range(start, end)
The second version is a single roaring range-set operation instead of 1440 Python-level lookups. The CPU reduction was 18× for the median calendar and 240× for calendars with > 200 events per day. That single change paid for the salary of the engineer who wrote it for the next 40 years.
Every time you create a meeting, you are entering into an invisible contract with 1.5 billion other people that the system will:
The 22 sections and 26 appendices you have just read are the engineering embodiment of that contract. The fact that it feels simple is the highest compliment the system can receive.
Further Reading (primary sources)
rrule librariesIf you enjoyed this deep dive, the next post in the series will cover “Designing Google Meet at Planetary Scale: WebRTC, SFU topologies, simulcast, and the 1:100,000,000 bandwidth problem.”
Thank you for reading. Now go schedule something important — and appreciate the infrastructure that just made it possible.