All Articles
CategoryiOS
Reading Time
12 min read
Published
2026-05-13
Word Count
710words

Grab a coffee — this one is a deep dive!

Case Study: TahminApp — 500K Users, 99.9% Uptime, 6 Months in Production

Summary

Real-time prediction notifications and a scalable data pipeline. Firestore + Cloud Messaging + Cloudflare Workers serving 500K MAU, p99 latency of 180ms, 99.94% uptime — and the real bottleneck was not where we expected it.

  • 500K MAU, p99 latency of 180ms, 99.94% uptime, and zero major incidents in 6 months
  • The real bottleneck turned out to be APNs delivery latency, not Firestore (4–6 minute delays)
  • After priority routing, 500K pushes were delivered in 25–35 seconds
  • A sharded collection (20 shards) raised Firestore write throughput by 20x
Case Study: TahminApp — 500K Users, 99.9% Uptime, 6 Months in Production

# Case Study: TahminApp — 500K Users, 99.9% Uptime, 6 Months in Production

Problem: On a sports prediction platform, instantly push real-time match events (goals, cards, halftime) to 500K+ users while updating live scores in the UI.

Result: 6 months in production, 500K MAU, p99 latency 180ms, 99.94% uptime, 0 incidents (>30min downtime).

Pro Tip: The bottleneck is not where you expect it. For us it wasn't Firestore writes — it was push delivery routing. Every big scaling story starts the same way.

1. Architecture Overview

3 tiers:

  • Edge layer (CF Workers): Webhook receiver, dedup, fan-out trigger
  • Backend (Firestore + Cloud Functions): Persistent state, business logic
  • Mobile (iOS native + Flutter Android): Realtime listener + push notification handler
swift
1[Sports Data Feed]
2 ↓ webhook
3[CF Worker — dedup + transform]
4 ↓ event
5[Cloud Function — match state update]
6 ↓ write
7[Firestore — predictions, users, scores]
8 ↓ realtime listener
9[Mobile Apps — UI update]
10
11[FCM/APNs — push notification]

2. The Bottleneck Story

First hypothesis: Firestore writes don't scale. Didn't hold up.

Second hypothesis: Cloud Functions cold starts are the problem. Didn't hold up either (warm instance pool).

The real bottleneck: APNs delivery latency. Apple's production token routing was delivering 500K pushes in 4–6 minutes instead of 30 seconds. During big-match events, "late notification" complaints spiked to 12%.

Fix: Priority routing — match-event pushes use APNs apns-priority 10 plus parallel batch sends from Cloudflare Workers. 500K pushes now deliver in 25–35 seconds. Complaints dropped from 12% to 0.3%.

3. Firestore Design

500K users × 50 predictions/user × 100 matches/season = 2.5M documents. A naive /predictions/{id} design would have collapsed.

The right pattern — sharded collection:

swift
1/predictions/shard-{0-19}/items/{predictionID}

Shard count is 20, assigned from a hash of the prediction ID. Each shard has its own index and its own write limit. Total write throughput: 20x.

Realtime listener: Mobile listens to /predictions/shard-{userShard} — only its own shard. Network savings: 95%.

4. Cloud Functions Patterns

40+ Cloud Functions in total. The 3 most critical:

  1. `onMatchEvent`: webhook trigger, 30-second dedup window, state update + push trigger
  2. `onPredictionWrite`: leaderboard recalculation (sharded counter pattern)
  3. `onUserWrite`: profile cache invalidation (Cloudflare KV purge)

Every function:

  • Memory: 256MB (most), 1GB (leaderboard)
  • Timeout: 60s
  • Max instances: 1000 (auto-scale)
  • Retry: 3x exponential backoff
  • Idempotency: Firestore set-based dedup on eventID

5. iOS App Architecture

iOS native (SwiftUI, iOS 16+):

  • TCA (The Composable Architecture) for state management
  • Firestore Snapshot Publisher — Combine pattern
  • APNS Notification Service Extension — payload decryption + smart actions
  • Local cache (SwiftData) — offline read-only mode

Memory baseline: 95MB. Peak (live match-time UI): 145MB.

Crash-free session rate: 99.84% (Crashlytics, 6-month average).

6. Cost Profile

Monthly infrastructure cost (at 500K MAU peak):

Item
Cost
Firestore (read/write/storage)
$420
Cloud Functions (invocations)
$180
Cloud Messaging (FCM)
$0 (free tier is enough)
APNs
$0 (Apple side)
Cloudflare Workers
$25 (Pro plan)
Cloudflare KV
$5
Hetzner monitoring server
$7
Total
$637/month

Per-user cost: $0.00127/user/month. Outstanding unit economics for an indie project.

7. Lessons Learned

1. The predicted bottleneck is rarely the real bottleneck. We only realized APNs delivery was the problem 4 weeks after launch.

2. Pay for sharding up front. Adding sharding after you've already hit 500K users is 4 months of technical debt.

3. Idempotency via event ID is critical — webhook retries were silently creating duplicates.

4. The Notification Service Extension is gold for content modification and alert prioritization.

5. Doing dedup + transform at the edge layer (CF Workers) cut Cloud Functions cost by 4x.

6. Run Crashlytics and custom telemetry together. Crashlytics gives you aggregation, custom telemetry gives you decision-making data.

7. In App Store reviews, average notification latency during big match moments is what users comment on the most. Optimizing latency took the 5-star rate from 62% to 78%.

Bottom line: Scaling to 500K production users is usually not a "Firestore problem" — it's a "delivery infrastructure" problem. Design the architecture as multi-tier from day one, then measure and refactor.

Tags

#Case Study#Production#Firestore#Push Notifications#Cloudflare Workers#Scaling
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

Lead Mobile Engineer with 12+ years of experience. Expert in iOS, Android and cross-platform architectures with Swift, SwiftUI, Kotlin and Flutter. I build performant, user-friendly mobile apps.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

We respect your privacy. You can unsubscribe at any time.

Share

Related Articles

iOS

SwiftData in Production: 6 Months of Real-World Experience and 3 Scenarios That Sent Us Back to Core Data

Production experience with SwiftData on iOS 17+. Migration pitfalls coming from Core Data, the practical realities of concurrency, real performance baseline numbers, and why we went back to Core Data on some projects.

14 min read
iOS

Case Study: ESP-Point — IoT Hardware + iOS Sync, Offline-First Architecture

ESP32 device ↔ iOS app bidirectional sync, BLE handshake + Firestore + Realm offline cache. 50ms handshake, 3-day offline survival, OTA firmware update. BLE state machine fragmentation is the real story.

13 min read
iOS

Case Study: MADPAW — Pet Tracker, GPS + Activity ML, 30-Day Battery Life

GPS tracker + activity classification, targeting 30+ days of battery life. Low-power location, on-device ML, scheduled fix patterns. 32-day test passed, 95% activity accuracy, 12K devices.

12 min read
iOS

Swift 6.2 and Post-WWDC26: Is Concurrency Really "Approachable" Now?

How default actor isolation, the @concurrent attribute, and nonisolated(nonsending) from SE-0461 and SE-0466 tore down the Swift 6 strict concurrency wall — and what to watch for in a production migration.

10 min read
iOS

iOS 26 Liquid Glass: A Guide to Adapting Your SwiftUI App to the New Material System

From glassEffect APIs to tabBarMinimizeBehavior, from the UIDesignRequiresCompatibility opt-out to GlassEffectContainer performance traps — the real decisions I faced migrating a production SwiftUI codebase to iOS 26's Liquid Glass language.

13 min read