LoadForge LogoLoadForge

Firebase Firestore Load Testing with LoadForge

Firebase Firestore Load Testing with LoadForge

Introduction

Firebase Firestore is designed to scale automatically, but “auto-scaling” does not mean “infinite performance.” If your application depends on Firestore for user profiles, product catalogs, chat messages, order processing, or analytics events, you need to understand how it behaves under real traffic. A proper Firebase Firestore load testing strategy helps you measure document read and write throughput, latency under concurrency, and how your data model performs during traffic spikes.

With LoadForge, you can run cloud-based load testing against Firestore-backed workloads using Locust scripts, then analyze response times, failure rates, and throughput in real time. This is especially useful when you want to validate performance before a launch, stress test a new feature, or compare the impact of indexing and query changes. Because LoadForge supports distributed testing, global test locations, and CI/CD integration, it’s a practical way to run repeatable Firestore performance testing at scale.

In this guide, you’ll learn how to load test Firebase Firestore using realistic Locust scripts that simulate:

  • Document reads by ID
  • Collection queries with filters and ordering
  • Authenticated writes using Firebase ID tokens
  • Mixed read/write workloads
  • Batch-style operations and contention scenarios

The examples use actual Firestore REST API patterns so you can adapt them directly to your environment.

Prerequisites

Before you begin load testing Firebase Firestore, make sure you have the following:

  • A Firebase project with Firestore enabled
  • A test dataset in Firestore, ideally separate from production
  • A Firebase Authentication setup if your app requires authenticated access
  • A service account or test user strategy for generating valid Firebase ID tokens
  • Your Firebase project ID
  • Knowledge of your target collections, fields, and common query patterns
  • A LoadForge account for running distributed load tests in the cloud

You should also decide whether you are testing:

  • Firestore directly via its REST API
  • Your own backend API that reads and writes Firestore
  • A hybrid flow involving Firebase Authentication and Firestore access

For direct Firestore load testing, common endpoints include:

  • Document read: GET https://firestore.googleapis.com/v1/projects/PROJECT_ID/databases/(default)/documents/users/user_123
  • Document write: PATCH https://firestore.googleapis.com/v1/projects/PROJECT_ID/databases/(default)/documents/users/user_123
  • Structured query: POST https://firestore.googleapis.com/v1/projects/PROJECT_ID/databases/(default)/documents:runQuery

If your Firestore rules require authentication, your load test must include a valid Authorization: Bearer <ID_TOKEN> header. For realistic performance testing, use the same auth model your application uses in production.

Understanding Firebase Firestore Under Load

Firestore is a document database optimized for flexible schema design and horizontal scaling, but performance depends heavily on access patterns. Load testing Firestore is not just about generating traffic; it’s about validating how your data model, indexes, and query design behave under concurrency.

Key Firestore behaviors to understand

Document reads

Single-document reads are typically fast and predictable, especially when fetching by document path. These are good baseline operations for measuring low-latency performance.

Collection and filtered queries

Queries can become slower when they rely on composite indexes, scan large collections, or return too many documents. Even if Firestore supports the query, response size and index usage affect latency.

Writes and contention

Firestore handles high write throughput well, but hot documents or hot key ranges can become bottlenecks. If many virtual users update the same document, you may see increased latency, retries, or contention-related issues.

Index overhead

Every write may update one or more indexes. A document with many indexed fields can cost more to write than expected. Load testing helps reveal whether write-heavy workloads are being slowed by indexing strategy.

Security rules impact

Firestore security rules add request evaluation overhead. This usually isn’t a major problem, but under high load it can contribute to latency. Testing with realistic authentication and rules is important.

Common Firestore bottlenecks

When running performance testing on Firebase Firestore, these are the most common issues teams uncover:

  • Repeated queries against large collections without pagination
  • Missing or inefficient composite indexes
  • Too many writes to the same document
  • Large document payloads increasing network and serialization costs
  • Overly chatty application patterns with many small requests
  • Authentication token refresh issues during sustained load
  • Unrealistic test data that doesn’t reflect production cardinality

The goal of load testing is to reproduce these conditions safely and measure where the system begins to degrade.

Writing Your First Load Test

Let’s start with a simple Firestore load test that reads user profile documents by ID. This is a good baseline because it isolates document-read latency without introducing query complexity.

Basic Firestore document read test

python
from locust import HttpUser, task, between
import random
 
PROJECT_ID = "my-firebase-project"
DATABASE_PATH = f"/v1/projects/{PROJECT_ID}/databases/(default)/documents"
 
USER_IDS = [f"user_{i}" for i in range(1, 1001)]
 
class FirestoreReadUser(HttpUser):
    wait_time = between(1, 3)
    host = "https://firestore.googleapis.com"
 
    @task
    def read_user_document(self):
        user_id = random.choice(USER_IDS)
        path = f"{DATABASE_PATH}/users/{user_id}"
 
        with self.client.get(
            path,
            name="Firestore GET /users/:id",
            catch_response=True
        ) as response:
            if response.status_code == 200:
                response.success()
            elif response.status_code == 404:
                response.failure(f"User document not found: {user_id}")
            else:
                response.failure(f"Unexpected status: {response.status_code} - {response.text}")

What this test does

This script simulates users reading profile documents from a users collection. It randomly selects IDs from a pool of 1,000 documents, which is more realistic than repeatedly hitting the same record.

Why this is useful

This test helps you answer basic performance questions such as:

  • What is the average latency for document reads?
  • How many reads per second can Firestore sustain for this collection?
  • Are there regional latency issues from different LoadForge test locations?
  • Do errors appear as concurrency increases?

When you run this in LoadForge, you can scale up to thousands of virtual users and observe how read performance changes in real time.

Advanced Load Testing Scenarios

Once you’ve validated baseline reads, the next step is to simulate more realistic Firestore workloads. Most production systems involve authentication, filtered queries, and writes. The following examples reflect those patterns.

Scenario 1: Authenticated Firestore reads with Firebase ID tokens

If your Firestore security rules require authenticated access, your load test should include Firebase Authentication. In many environments, you will generate test user ID tokens ahead of time and store them as environment variables or test data inputs.

This example uses a pre-generated list of Firebase ID tokens and performs authenticated reads against an orders collection.

python
from locust import HttpUser, task, between
import random
import os
 
PROJECT_ID = "my-firebase-project"
DATABASE_PATH = f"/v1/projects/{PROJECT_ID}/databases/(default)/documents"
 
ORDER_IDS = [f"order_{i}" for i in range(1000, 2000)]
 
FIREBASE_TOKENS = [
    os.getenv("FIREBASE_TOKEN_1"),
    os.getenv("FIREBASE_TOKEN_2"),
    os.getenv("FIREBASE_TOKEN_3"),
]
 
class AuthenticatedFirestoreReads(HttpUser):
    wait_time = between(1, 2)
    host = "https://firestore.googleapis.com"
 
    def on_start(self):
        self.token = random.choice([t for t in FIREBASE_TOKENS if t])
 
    @task
    def read_order_document(self):
        order_id = random.choice(ORDER_IDS)
        path = f"{DATABASE_PATH}/orders/{order_id}"
        headers = {
            "Authorization": f"Bearer {self.token}"
        }
 
        with self.client.get(
            path,
            headers=headers,
            name="Firestore Auth GET /orders/:id",
            catch_response=True
        ) as response:
            if response.status_code == 200:
                response.success()
            elif response.status_code in (401, 403):
                response.failure(f"Auth failure: {response.status_code} - {response.text}")
            else:
                response.failure(f"Unexpected status: {response.status_code} - {response.text}")

Why this scenario matters

Authenticated traffic is often slower than public reads because it includes token validation and security rule evaluation. If your real application uses Firestore rules to restrict access, this is the only meaningful way to benchmark performance.

For more realistic stress testing, use a larger pool of tokens mapped to actual test users with distinct data access patterns.

Scenario 2: Structured queries for product search and pagination

Many Firestore applications rely on filtered and sorted queries instead of direct document reads. This example simulates an e-commerce workload where users browse active products by category and price range.

python
from locust import HttpUser, task, between
import random
import json
 
PROJECT_ID = "my-firebase-project"
RUN_QUERY_PATH = f"/v1/projects/{PROJECT_ID}/databases/(default)/documents:runQuery"
 
CATEGORIES = ["electronics", "books", "home", "fitness", "toys"]
 
class FirestoreProductQueries(HttpUser):
    wait_time = between(2, 4)
    host = "https://firestore.googleapis.com"
 
    @task
    def query_active_products(self):
        category = random.choice(CATEGORIES)
        max_price = random.choice([25, 50, 100, 250])
 
        payload = {
            "structuredQuery": {
                "from": [
                    {
                        "collectionId": "products"
                    }
                ],
                "where": {
                    "compositeFilter": {
                        "op": "AND",
                        "filters": [
                            {
                                "fieldFilter": {
                                    "field": {"fieldPath": "category"},
                                    "op": "EQUAL",
                                    "value": {"stringValue": category}
                                }
                            },
                            {
                                "fieldFilter": {
                                    "field": {"fieldPath": "active"},
                                    "op": "EQUAL",
                                    "value": {"booleanValue": True}
                                }
                            },
                            {
                                "fieldFilter": {
                                    "field": {"fieldPath": "price"},
                                    "op": "LESS_THAN_OR_EQUAL",
                                    "value": {"integerValue": str(max_price)}
                                }
                            }
                        ]
                    }
                },
                "orderBy": [
                    {
                        "field": {"fieldPath": "price"},
                        "direction": "ASCENDING"
                    }
                ],
                "limit": 20
            }
        }
 
        headers = {
            "Content-Type": "application/json"
        }
 
        with self.client.post(
            RUN_QUERY_PATH,
            data=json.dumps(payload),
            headers=headers,
            name="Firestore POST runQuery products",
            catch_response=True
        ) as response:
            if response.status_code == 200:
                try:
                    results = response.json()
                    if isinstance(results, list):
                        response.success()
                    else:
                        response.failure("Unexpected query response format")
                except Exception as e:
                    response.failure(f"JSON parse error: {e}")
            else:
                response.failure(f"Query failed: {response.status_code} - {response.text}")

What this test reveals

This query-focused load test is useful for identifying:

  • Slow queries caused by poor indexing
  • Performance differences between categories with different document counts
  • Latency increases as virtual users scale up
  • Whether result size and ordering are affecting response times

If Firestore returns index-related errors during testing, that’s a strong signal that your production query path is not fully prepared for load.

Scenario 3: Mixed read/write workload for carts and checkout activity

Real applications rarely do only reads or only writes. A more realistic Firestore performance testing scenario mixes document fetches, cart updates, and order creation. This example simulates a retail workload with authenticated users.

python
from locust import HttpUser, task, between
import random
import json
import os
import uuid
from datetime import datetime, timezone
 
PROJECT_ID = "my-firebase-project"
DATABASE_PATH = f"/v1/projects/{PROJECT_ID}/databases/(default)/documents"
 
USER_IDS = [f"user_{i}" for i in range(1, 501)]
PRODUCT_IDS = [f"prod_{i}" for i in range(1, 2001)]
TOKENS = [os.getenv("FIREBASE_TOKEN_1"), os.getenv("FIREBASE_TOKEN_2"), os.getenv("FIREBASE_TOKEN_3")]
 
class FirestoreMixedWorkload(HttpUser):
    wait_time = between(1, 3)
    host = "https://firestore.googleapis.com"
 
    def on_start(self):
        self.user_id = random.choice(USER_IDS)
        self.token = random.choice([t for t in TOKENS if t])
 
    def auth_headers(self):
        return {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json"
        }
 
    @task(5)
    def read_cart(self):
        path = f"{DATABASE_PATH}/carts/{self.user_id}"
 
        with self.client.get(
            path,
            headers={"Authorization": f"Bearer {self.token}"},
            name="Firestore GET /carts/:userId",
            catch_response=True
        ) as response:
            if response.status_code == 200:
                response.success()
            elif response.status_code == 404:
                response.success()  # Empty cart is valid in many apps
            else:
                response.failure(f"Cart read failed: {response.status_code} - {response.text}")
 
    @task(3)
    def update_cart(self):
        product_id = random.choice(PRODUCT_IDS)
        quantity = random.randint(1, 3)
        path = f"{DATABASE_PATH}/carts/{self.user_id}?updateMask.fieldPaths=items&updateMask.fieldPaths=updatedAt"
 
        payload = {
            "fields": {
                "items": {
                    "arrayValue": {
                        "values": [
                            {
                                "mapValue": {
                                    "fields": {
                                        "productId": {"stringValue": product_id},
                                        "quantity": {"integerValue": str(quantity)}
                                    }
                                }
                            }
                        ]
                    }
                },
                "updatedAt": {
                    "timestampValue": datetime.now(timezone.utc).isoformat()
                }
            }
        }
 
        with self.client.patch(
            path,
            data=json.dumps(payload),
            headers=self.auth_headers(),
            name="Firestore PATCH /carts/:userId",
            catch_response=True
        ) as response:
            if response.status_code == 200:
                response.success()
            else:
                response.failure(f"Cart update failed: {response.status_code} - {response.text}")
 
    @task(1)
    def create_order(self):
        order_id = str(uuid.uuid4())
        path = f"{DATABASE_PATH}/orders/{order_id}"
 
        payload = {
            "fields": {
                "userId": {"stringValue": self.user_id},
                "status": {"stringValue": "pending"},
                "totalAmount": {"doubleValue": random.uniform(20.0, 250.0)},
                "createdAt": {"timestampValue": datetime.now(timezone.utc).isoformat()},
                "source": {"stringValue": "load-test"}
            }
        }
 
        with self.client.patch(
            path,
            data=json.dumps(payload),
            headers=self.auth_headers(),
            name="Firestore PATCH /orders/:id",
            catch_response=True
        ) as response:
            if response.status_code == 200:
                response.success()
            else:
                response.failure(f"Order creation failed: {response.status_code} - {response.text}")

Why mixed workloads are critical

This is often the most useful kind of load test because it reflects actual application behavior. Reads dominate traffic, writes happen regularly, and a small percentage of users perform more expensive operations like checkout.

This kind of script is ideal for LoadForge because you can run it from multiple cloud regions, compare response times geographically, and watch error rates in real time as traffic ramps up.

Optional: generating Firebase ID tokens for test users

If you use email/password test accounts, you can generate a token with the Firebase Identity Toolkit API.

bash
curl -X POST "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=YOUR_WEB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "loadtest1@example.com",
    "password": "StrongPassword123!",
    "returnSecureToken": true
  }'

The response includes an idToken you can use in your Locust headers. In larger test environments, teams usually pre-generate and rotate many tokens rather than signing in during the test itself.

Analyzing Your Results

After running your Firebase Firestore load test in LoadForge, focus on a few key metrics.

Response time percentiles

Average latency is useful, but percentiles are more important. Watch:

  • P50 for normal user experience
  • P95 for degraded but still common experiences
  • P99 for tail latency under stress

Firestore may look healthy at the average level while still producing unacceptable P95 or P99 latency.

Requests per second

Track throughput for each operation type:

  • Single-document reads
  • Structured queries
  • Writes and updates

If throughput plateaus while latency rises sharply, you may be approaching a practical limit in your workload design.

Error rates

Pay close attention to:

  • 401 or 403 authentication and rules issues
  • 404 errors due to invalid test data
  • 429 or quota-related behavior
  • 5xx responses from dependent services or gateways
  • Query/index errors returned by Firestore

A realistic performance testing run should distinguish between expected functional issues and genuine scaling problems.

Endpoint-level comparison

Name your Locust requests clearly, as shown in the examples. This lets you compare:

  • Firestore GET /users/:id
  • Firestore POST runQuery products
  • Firestore PATCH /orders/:id

In LoadForge’s real-time reporting, these labels make it much easier to pinpoint which Firestore operation becomes the bottleneck.

Ramp behavior

Do not just run a flat test. Observe what happens during:

  • Ramp-up
  • Sustained steady state
  • Spike traffic
  • Ramp-down

Firestore often behaves differently during traffic spikes than during gradual growth. LoadForge’s distributed testing makes it easier to simulate both patterns.

Performance Optimization Tips

If your Firebase Firestore load testing reveals issues, these are the first areas to review.

Design for query efficiency

Use targeted queries with filters and limits. Avoid reading large collections when users only need a small subset of documents.

Validate indexes early

If structured queries are slow or failing, review your composite indexes. Index design is one of the most important factors in Firestore performance testing.

Reduce hot document contention

Avoid having many users update the same document, such as a global counter or shared cart state. Shard counters or redesign write-heavy flows where needed.

Keep documents lean

Large documents increase network overhead and serialization time. Split infrequently used fields into separate documents if necessary.

Use realistic pagination

Never load test with artificially tiny result sets if production queries return much more data. Pagination patterns should match real user behavior.

Separate test and production data

Use a dedicated Firestore dataset for stress testing. This prevents noisy metrics and protects production workloads.

Test from multiple regions

If your users are global, run tests from different LoadForge locations to understand network impact and regional latency differences.

Common Pitfalls to Avoid

Testing only happy-path reads

A Firestore system that performs well on direct document reads may still struggle with filtered queries or write contention. Include a mix of operations.

Reusing the same document IDs

If every virtual user hits the same few documents, you create unrealistic hotspots. Use broad, production-like datasets.

Ignoring authentication overhead

If production traffic is authenticated, unauthenticated tests will understate real latency. Always include realistic token usage and security rules evaluation.

Using synthetic queries that don’t match the app

Load testing is only useful when it reflects actual user behavior. Query the same collections, fields, and sort patterns your application uses.

Not accounting for quotas and billing

Firestore load testing can generate significant read and write volume. Make sure you understand the cost implications and project quotas before running large tests.

Logging in during every request

Authentication flows can dominate the test if done incorrectly. In most cases, pre-generate Firebase ID tokens and reuse them for a realistic session duration.

Skipping steady-state testing

Spike tests are valuable, but steady-state load testing is where memory, connection, and indexing issues often become visible over time.

Conclusion

Firebase Firestore is powerful, but its real-world performance depends on your document model, indexing strategy, authentication rules, and traffic patterns. By running structured Firebase Firestore load testing with realistic Locust scripts, you can measure read and write latency, uncover scaling bottlenecks, and validate that your application is ready for production traffic.

LoadForge makes this process easier with cloud-based infrastructure, distributed testing, real-time reporting, global test locations, and CI/CD integration for repeatable performance testing. Whether you’re validating a new launch, stress testing a write-heavy workload, or optimizing query performance, Firestore load testing gives you the data you need to improve confidently.

Try LoadForge to run your Firebase Firestore load tests at scale and see how your database performs before your users do.

Try LoadForge free for 7 days

Set up your first load test in under 2 minutes. No commitment.