Skip to main content

Running with shackles: base64 in Firestore on the free tier

Share:XLinkedInHN
Cover for Running with shackles: base64 in Firestore on the free tier

The moment

I remember the exact page. Firebase console, billing tab, the little card that says Blaze. It wants a credit card. I was in college, building the official Android app for the Fine Arts Club at PSG Tech (the repo is PSGTech-FineArts-Android-App). I did not have a credit card that I wanted to put on a Google product that could, in theory, run up a bill overnight if I got a loop wrong.

The Spark plan was fine for Firestore. It was fine for Auth. It was fine for a decent chunk of realtime database traffic. What it was not fine for was Cloud Storage. Storage sat behind Blaze. If I wanted to upload a profile picture, or an event photo, or any user-generated image from the club app, I had to hand Google a card first.

I did not hand Google a card. I base64-encoded the image and wrote it into a Firestore document.

The hack, in one paragraph

Bitmap becomes ByteArrayOutputStream through compress(Bitmap.CompressFormat.JPEG, 60, ...). The byte array becomes a string through Base64.encodeToString(bytes, Base64.NO_WRAP). The string goes into a field called imageData on a Firestore document. When the app needs to display the image, it reads the document, pulls the string, runs it through Base64.decode, and hands the resulting ByteArray to BitmapFactory.decodeByteArray. The ImageView gets the bitmap. The user sees their profile picture. Nobody knows.

That is the whole trick. It is stupid and it works.

What it actually costs

Base64 encoding grows a payload by about 33 percent. Three bytes of binary become four bytes of ASCII. A 200 KB JPEG lands in Firestore as roughly 267 KB of string. That is the first tax.

The second tax is the Firestore document size cap. A single document cannot exceed 1 MB. That sounds huge until you remember that a phone camera in 2020 will happily hand you a 6 MB JPEG straight from the sensor. So the compression step is not optional. It is load-bearing. I was compressing at quality 60, sometimes 40, and downscaling the longest edge to 1024 pixels before the encode. Every image passed through a scaling and quality gate that had nothing to do with the app and everything to do with keeping the document under a megabyte.

The third tax is retrieval. Firestore charges per document read. Storage charges per byte transferred. Under the free tier, both had generous quotas, so I did not immediately pay the price. But every image display was now: read a document, receive a large string field, decode base64 on the main thread if I forgot to move it, allocate a ByteArray, hand it to BitmapFactory, and then the bitmap allocation itself. Four allocations to render one photo. Doing this in a RecyclerView was a genuine problem.

The fourth tax is memory. BitmapFactory.decodeByteArray allocates the full decoded bitmap. A 1024 by 1024 JPEG at ARGB_8888 is 4 MB in RAM. Show ten of them in a scrolling list and you are at 40 MB before any other object exists. I had at least one crash report that traced to this, on an older phone with 2 GB of RAM total.

What I should have done, and what I did instead

The right answer was Cloudinary. Or imgbb. Or B2, which had a real free tier for storage. Or my own $5 VPS with nginx and a folder. There were half a dozen paths to a real image URL that did not require a credit card. I knew about at least three of them.

I chose base64 in Firestore anyway. Not because I did not know better. Because I did not want to break the shape of the app. Everything else was Firebase. Auth was Firebase, the user profile document was Firestore, the app already had a FirebaseFirestore.getInstance() call at the top of every activity that mattered. Adding a second backend meant a second SDK, a second auth boundary, a second failure mode. Base64 was one line in the write path and one line in the read path. It kept the mental model whole.

That is the trade the free tier actually forces. Not "cheap versus expensive." Complexity versus complexity. You either add a new service and pay in integration cost, or you abuse the service you already have and pay in efficiency. I picked the second one, and I do not entirely regret it.

What building under a free tier taught me

Every real engineering decision I made in that year was a constraint decision. Not "what is the best way to do this," but "what is the best way to do this given that I have zero dollars, no cloud budget, no cloud budget request that will ever get approved, and one laptop that is definitely not a build machine."

That is a good school to attend. It taught me:

Compression is a design choice, not a build step. If the payload is the bottleneck, choosing quality 60 versus 80 is architecture, not tuning.

Document databases are not object stores, but they are also not not object stores. Firestore is happy to hold a 900 KB string. It just charges you for it in ways you notice later.

The instinct to reach for the paid tier is often laziness dressed as good taste. The paid tier is correct. The free tier forces you to think about size, latency, and cost per operation from day one, which the paid tier lets you defer for eighteen months until it becomes a rewrite.

The honest reflection

I do not think base64 in Firestore is a good pattern. I would not recommend it. If a colleague showed me that in a PR today I would ask them to use Cloudinary and I would help them wire it up in an afternoon.

But I did not learn what a bitmap decode actually costs by reading a blog post about bitmap decodes. I learned it because the club app I was writing kept OOMing on my roommate's Redmi. I learned about Firestore document limits because I hit them with a photo of a whiteboard from an event. I learned about the 33 percent base64 overhead because it was the difference between a document fitting and not fitting.

The free tier was not a limitation I overcame. It was the teacher. Every trick I have for keeping payloads small, every reflex to check what a service actually charges for versus what it advertises, every instinct to look at the storage line item before the compute line item, came from that year of running with the credit card stayed in my wallet.

I ship things now that use Blaze plans and paid Supabase tiers and Modal credits and OpenAI budgets that would have terrified twenty-year-old me. But the twenty-year-old is still the one who checks the bill.

See also

Cite as: Saravanan, K. (2026). Running with shackles: base64 in Firestore on the free tier. Kaushik Saravanan. https://www.kaushik.cv/blog/firebase-free-tier-base64-android