Real-time multiplayer is one of the hardest problems in app development. After shipping games to production with Firebase, I learned that most architecture decisions involve tradeoffs that tutorials don’t mention.
This post covers what works, what breaks, and where the limits are.
Scalability: Know Your Limits
The most important lesson: Firestore-based multiplayer has hard limits. Here’s what I learned shipping to production:
Performance by player count
- 2-10 players: Works well with denormalized documents
- 10-20 players: 0.5-2 second lag becomes noticeable
- 20+ players: Document contention causes failures
Why it breaks down
- Firestore limits writes to ~1/second per document before performance degrades
- With denormalized player data, every update triggers document writes
- 20 players updating simultaneously means 20 writes to the same document
- O(N²) problem: N players × N updates = exponential snapshot notifications
Solutions that scale
- Subcollections: Store each player’s data in separate documents (
/quizzes/{id}/players/{userId}
) - Hybrid architecture: Use Firebase Realtime Database for ephemeral game state, Firestore for persistence
- Sharding: Split large games across multiple coordination documents
The naive approach (single denormalized document) works for small groups but fails at scale. Design for your target player count from day one.
Security: App Check & Rate Limiting
Your multiplayer game will be attacked. Plan for it.
Firebase App Check
- Enable enforcement mode from day one, even in beta
- Configure debug tokens early (check Xcode console logs for iOS, logcat for Android)
- Provide fallbacks for rooted/jailbroken devices
- Better to catch integration issues early than face surprise bills at launch
Rate limiting essentials
- Track both user ID and hashed IP (dual-layer protection)
- Use Firestore transactions for atomic counter updates
- Fail closed on errors (never accidentally allow unlimited access)
- Different limits for different operations (game joins vs. player updates)
Firestore Document Design
Firestore is not SQL. Design for your queries, not normalized relations.
Denormalization works for small games
- Store player data inline for fewer than 10 players
- One read gets everything needed to render game state
- Eliminates N+1 query patterns
- Pre-calculate aggregations (totals, averages, leaderboard positions)
Document size limits bite fast
- 1MB limit per document
- ~100 players with profiles hits the limit
- ~1000 players with just IDs and scores
- Solution: Use subcollections when scaling beyond small groups
Tradeoffs
- Too much denormalization: Hit size limits, contention issues
- Too little denormalization: Multiple reads increase latency and cost
- Sweet spot: Denormalize critical data, use subcollections for player-specific state
Security Rules
Never trust the client. Validate everything server-side.
Key patterns
- Users read their own data, Cloud Functions handle writes
- Public games readable by authenticated users, private games need access codes
- Store social features (friends, blocks) in separate collections with strict rules
- All deletions go through Cloud Functions
Real-Time Synchronization
Transactions prevent race conditions
- Use Firestore transactions for atomic read-modify-write operations
- Validate game state before updates (check expiration, turn order, etc.)
- Each update is atomic and consistent even with simultaneous submissions
- Caveat: Transaction contention increases with concurrent writes (see scalability limits above)
Optimistic updates keep things responsive
- Apply changes locally for instant feedback
- Run server transaction in background
- Reconcile when server responds
- Rollback on errors or conflicts
Listener management
- One listener per game session, distribute updates locally
- Clean up aggressively to prevent memory leaks
- Watch costs: Each client listening to large documents = expensive bandwidth
Performance & Offline Handling
Batch writes where possible
- Group related updates in single commit
- Faster and cheaper than individual writes
Cache intelligently
- Static data (rules, assets) cached indefinitely
- Player profiles with TTL
- Invalidate on events, not timers
Firestore handles offline automatically
- Local caching and write queuing built-in
- No need for complex offline logic
- Handle
fromCache
flag in UI for offline indicators - Time-sensitive games: Pause timers when disconnected, prevent actions until reconnected
Testing
Simulate real network conditions in development
- Add random latency (100-2000ms)
- Inject occasional failures (10% error rate)
- Test with bot players: variable response times, disconnections, edge cases
What Matters
Know your scale before you design
- Denormalized Firestore works well for 2-10 players
- Beyond 20 players, you need subcollections or a hybrid architecture
- Document contention and O(N²) notification patterns will break naive implementations
Security can’t be retrofitted
- Enable App Check enforcement from day one
- Server-side validation for all writes
- Rate limiting protects your wallet and users
Design for Firestore, not against it
- Structure documents for queries, not normalized relations
- Pre-calculate aggregations
- Watch document size limits (1MB hits fast)
Test with realistic conditions
- Real networks have latency and failures
- Offline mode is not optional
- Bot testing catches race conditions
What Firestore Doesn’t Solve
This architecture has clear limits:
- Real-time games beyond 20 players need different approaches (Realtime Database, WebSockets, dedicated game servers)
- Document contention is a hard constraint, not a performance tuning problem
- Costs scale with reads, not just writes (every client listening = bandwidth charges)
- No server-authoritative physics or anti-cheat (client trust required for gameplay)
Firebase works well for small-to-medium multiplayer games. The key is understanding where the platform stops scaling and planning accordingly.