Skip to main content

Running with shackles: base64 in Firestore on the free tier

Firebase Storage sat behind Blaze. The Fine Arts Club Android app base64-encoded every image and stuffed it into a Firestore document. A memoir about that.

By Kaushik Saravanan
ShareXLinkedInHN
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.

It is a silly workaround, 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 because I did not want to split the app across two backends. Auth and the user profile already used Firebase, and every activity that mattered called FirebaseFirestore.getInstance(). A second backend meant another SDK, another auth boundary, and another failure mode. Base64 took one line in the write path and one in the read path. It kept the app's data in one place.

That choice saved integration work and cost storage efficiency. I could add a service and its SDK, or keep the images in Firestore with the code path the app already used. I picked the second option, and I do not entirely regret it.

What building under a free tier taught me

Most engineering decisions I made that year started with the same constraint: I had zero dollars, no approved cloud budget, and one laptop that was not a build machine. I looked for the best solution I could build within those limits.

Working within those limits 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