If the Firebase Admin SDK on Vercel is running up your Firestore reads, check the transport before you check your queries. On Kata Jaam, the default gRPC transport wedged on reused Vercel function instances and turned a cache into about three million Firestore reads a day. Switching the Admin SDK to REST fixed it, and the bill went back to a flat rate.
The symptom: a bill and a read graph that did not match the traffic
Kata Jaam is an event-discovery product for Nepal. The web side is three Next.js apps on Vercel, and they read from Firestore through the Firebase Admin SDK. Then the Firestore bill jumped.
The read graph is what gave it away. About three million reads a day is far more than the pages people were actually opening could explain. The reads did not follow the traffic curve.
How I found it
The bill came first. I opened the usage tab in the Firebase console and saw daily document reads far above anything the pages needed.
I ruled out the usual suspects first: a query that had changed, a cache key that varied per request, a crawler hitting the routes. None of them explained the volume. The extra reads came from reused function instances, and the one thing those instances carry from one invocation to the next is the open gRPC channel.
What I can say for certain is that the code that was supposed to serve from a cache was going back to Firestore far more often than it should have, and the cause was the connection underneath it, not the logic on top.
Why the Firebase Admin SDK on Vercel misbehaves over gRPC
By default the Firestore client inside the Admin SDK talks to Google over gRPC. gRPC keeps a long-lived HTTP/2 channel open and reuses it for every request. On a long-running server that is exactly what you want.
A serverless function is not a long-running server. Vercel keeps an instance warm and reuses it across invocations, and under Fluid Compute one instance can serve many requests and live for a long time. Between bursts of work the instance can sit idle or be paused. The network path under that open channel can be dropped in the meantime, and the client inside the instance does not always find out.
In general, a stale channel can turn into extra reads like this:
- A request goes out on a channel the client believes is alive, and stalls.
- The call waits until its deadline, then the client retries, sometimes more than once.
- Code that treats a slow or failed read as a cache miss falls through and reads again.
- Any streaming read that drops and reconnects can fetch its whole result set again, and each document fetched counts as a read.
Put enough warm instances and enough requests through that loop and a cache becomes a stampede. I have not confirmed which of these steps dominated in my case.
The fix: preferRest on the Firestore settings
The Firestore client accepts a setting that makes it use REST where it can instead of gRPC. Each REST request is a plain HTTPS call with nothing long-lived to go stale between invocations. It has to be set once, before the first read or write. The option name is preferRest:
import { getFirestore } from "firebase-admin/firestore";
const db = getFirestore();
db.settings({ preferRest: true });
Calling settings a second time on the same instance throws, so put it where the Firestore client is created, in one module that every route imports. As far as I know, operations that need a stream, such as real-time listeners, still use gRPC with this flag on. For request and response reads from a Next.js route, REST is the whole path.
The trade is a little latency per call, because there is no warm channel to reuse.
How to prove it worked
Do not trust the bill alone, because it lags. Watch the read count.
- Note the daily document reads in the Firebase console usage tab, or the Firestore read metric in Google Cloud Monitoring, for a few days before the deploy.
- Deploy the one change and nothing else, so the graph has one explanation.
- Compare the next few days against the same traffic.
On Kata Jaam the reads fell from about three million a day back in line with traffic, and the bill settled at a flat rate. If your reads do not move after the change, the transport was not your problem and you should go back to the queries.
What to check in your own project
I would ask these of any Next.js app on Vercel that reads Firestore through the Admin SDK:
- Does the read count follow your traffic, or does it climb on its own between deploys?
- Is the Firestore client created once per instance, or on every request?
- What does your cache do when a read is slow or fails: does it treat that as a miss and read again?
- Are any routes holding a listener or a stream open inside a function?
- Is
preferRestalready set, and is it set before the first call?
If more than one of those answers is uncomfortable, the transport is worth a single-change test.
The rule it became
It is now one of the decisions I list for Kata Jaam: keep the Admin SDK on REST on Vercel. I write these decisions down so the same bug does not happen twice.
If you want this kind of care in your product
I build mobile and web products end to end, including the boring parts like this, on build my app or product.