The top 10 things to know about Firestore when choosing a database for your app Doug Stevenson Follow Jan 1 · 13 min read

Something you might have noticed about database products is that there’s no “one-size-fits-all” product that meets all the needs of all possible project requirements. Each product is built for a specific realm of use cases, each very different from each other. For example:

SQLite is a small relational database, and meant to be embedded for use on devices (it’s the built-in choice for the Android platform).

Redis is an in-memory database, primarily representing key/value pairs, which is great for hosting large, fast caches.

Neo4j is a graph database which excels at querying complex data structures by traversing the nodes of a graph the expresses relationships between entities.

These are each very popular, and also very specialized, database products intended to solve different types of problems with software development. They also have limitations — SQLite doesn’t scale in the cloud, and Redis doesn’t have flexible querying. So how do you decide which database to choose? It’s about picking the right tool for the job. Choosing a database that doesn’t suit the needs of your project can cause problems, but you might not realize it until well after you’ve committed to the database.

To avoid long-term frustration with any given database, it’s important to understand its strengths and weaknesses. In the rest of this post, I’ll discuss the following 10 things you should know about Firestore that you might not already know:

Strengths

Massive scaling & predictably high performance in Google’s cloud

Realtime query results

Offline query support

Limitations

No native aggregation queries

Document write frequency limit of 1 per second

Document size limit of 1MB

Performance of offline/cached queries

Query flexibility and index management

Good to know / best practices

Security rules

Database triggers with Cloud Functions

Fortunately, the limitations have workarounds, so read on to learn about how to make Firestore work well for your project. I’m going to assume you already know a little bit about how Firestore works, as a NoSQL, document-oriented database. So, if you haven’t already, read up on it now.

Strengths

1. Massive scaling & high performance in Google’s cloud

You can add billions of documents to a collection, and it won’t skip a beat. As long as you’re willing to pay for the cloud-hosted storage consumed by both the documents and indexes created on them, you won’t have any trouble with storage, or the performance of queries against those documents. The performance of queries scale with the size of the query result set, not the size of the collection. This means that a query for 50 documents in a collection is always going have the same performance characteristics, no matter how large that collection is.

This behavior is not very intuitive if you come from a traditional SQL background, where the performance of most databases break down as the number of rows in a table grows very large. Firestore removes that limitation, without you having to do any work. On top of that, Firestore doesn’t let you perform an inefficient query. In SQL terms, it refuses to do a “table scan” for queries that aren’t supported by an index. The downside of massively scalable queries is less flexible querying (e.g. point #4 below), and index management required to support all the various queries you need (point #8 below).

2. Realtime query results

A very special feature of Firestore that you probably already know about is the fact that you can get realtime updates from documents as they change. This not only applies to individual documents, but also the results of queries. When you perform a query, you can attach a listener to it, and receive realtime callbacks as the results of the query would change over time. This means that your query listener will be notified any time:

Documents are added or removed from the result set Documents change position in the ordering of results Document field values changes in any way The query becomes illegal because of a change in the way security rules evaluate for that query.

Realtime query results lets you keep the UI of your app up-to-date with the latest data, with very little extra effort. Here’s a JavaScript sample from the documentation that shows how to check for each type of change in a query listener. Notice that change.type contains the type of change to the document since the last time the callback function was invoked:

db.collection("cities").where("state", "==", "CA")

.onSnapshot(function(snapshot) {

snapshot.docChanges().forEach(function(change) {

if (change.type === "added") {

console.log("New city: ", change.doc.data());

}

if (change.type === "modified") {

console.log("Modified city: ", change.doc.data());

}

if (change.type === "removed") {

console.log("Removed city: ", change.doc.data());

}

});

});

3. Offline query support

Mobile apps often lose connectivity for a variety of reasons. Underground transportation, switching cell towers, and flaky WiFi connections all cause disruptions to internet service. One big advantage of using the Firebase SDKs to access Firestore is the fact that it will cache documents locally (web and mobile SDKs only). After you make a query that gets documents, the contents of those documents are persisted in a local cache on the device. This allows your app to remain usable if the app goes offline. The cache works for both reads and writes. Queries will be able to use documents from the local cache, and writes will also be stored locally, to be automatically synchronized to the cloud when the app regains connectivity. This cache can also save on the cost of reads of documents that are unchanged on the server. This is a huge convenience for app developers who want to write robust applications.

However, there is one potentially surprising downside to the local cache, which I will cover in point #7 below.

The above three strengths are very powerful features, especially since you don’t have to do any of the hard infrastructure work that makes them possible. However, like all databases, there are some limitations that you need to be aware of before building your project on it. Fortunately, these limitations have workarounds.

Limitations (and their workarounds)

4. No aggregation queries

SQL databases make it very easy to aggregate data over the contents of a table. You’re able to get the count, sum, max, average, and so on, of all values in a column. These types of queries don’t scale very well, as they require a read of each matching row of the query— there are no easy performance shortcuts. Since query performance at massive scale is extremely important in Firestore, it simply doesn’t offer these sort of expensive aggregation queries.

There are some scalable aggregation query alternatives provided in the documentation. These solutions involve either 1) making the computation in the client after reading all the necessary documents, or 2) the continual re-computation of the aggregate values every time an aggregated document value changes. But these solutions have their own limitations, as you have to know ahead of time exactly what you want to aggregate (and if not, make “catch up” computations after you decide what you want).

Even if you implement one of these alternatives, it might not scale in practice, because of the per-document write frequency limit, which I’ll talk about next.

5. Document write frequency limit of 1 per second

If your app isn’t ready for it, Firestore’s limits on writes and transactions are could throw a wrench in its ability to scale under load. The maximum sustained write rate to a document is 1 per second. You might be able to burst more frequent writes for a short amount of time, but after a while, writes will fail at higher sustained rates. This might not be something you observe during development, so it’s important to think about whether or not this could become a problem under load in production.

A common case where this rate limit becomes problematic is when you want to use a document to tally a count of events that might be happening quickly. For that reason, the documentation provides an alternative solution called a “distributed counter” that shards the counter across multiple documents. There is also a Firebase Extension that will set this up for you. This solution has its own limitations as well, so be sure to understand how this solution works before implementing it.

Suffice to say that Firestore isn’t really the best solution for individual values that must change rapidly. Firebase Realtime Database is better at this sort of thing, though you still might eventually have to shard in a much more code-friendly way. You can run that database side-by-side with Firestore with no issues, if you want.

You should also know that the maximum writes per second among all documents in a Firestore database is 10,000 per second. Though, in practice, I haven’t heard about that limit causing problems for anyone that requires a workaround.

6. Document size limit of 1MB

Firestore places a limit of 1MB on the amount of data that can be stored in a single document. This is more than enough for most cases. However, things start to break down when using map and array type fields that can grow unbounded over time.