<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[EtherNode]]></title><description><![CDATA[I write software related blogs]]></description><link>https://blog.saikat.in</link><generator>RSS for Node</generator><lastBuildDate>Mon, 27 Apr 2026 14:44:58 GMT</lastBuildDate><atom:link href="https://blog.saikat.in/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I Got gRPC Working Through Cloudflare Tunnel (The Hard Way)]]></title><description><![CDATA[A complete guide to exposing a Go gRPC backend behind college NAT using Cloudflare Tunnel, Docker Compose, and TLS, including every mistake I made along the way.


Background
I'm a student building Bl]]></description><link>https://blog.saikat.in/how-i-got-grpc-working-through-cloudflare-tunnel-the-hard-way</link><guid isPermaLink="true">https://blog.saikat.in/how-i-got-grpc-working-through-cloudflare-tunnel-the-hard-way</guid><category><![CDATA[gRPC]]></category><category><![CDATA[cloudflare]]></category><category><![CDATA[self-hosted]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Saikat Das]]></dc:creator><pubDate>Sat, 28 Mar 2026 01:25:47 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>A complete guide to exposing a Go gRPC backend behind college NAT using Cloudflare Tunnel, Docker Compose, and TLS, including every mistake I made along the way.</p>
</blockquote>
<hr />
<h2>Background</h2>
<p>I'm a student building <strong>Bluppi</strong>, a Flutter app with a Go gRPC backend. My server runs on my laptop on college WiFi, behind a NAT I don't control, no port forwarding, no static IP.</p>
<p>My stack:</p>
<ul>
<li><p><strong>Backend</strong>: Go gRPC server (<code>bluppi-api:50051</code>) + Go gRPC gateway (<code>bluppi-gateway:50050</code>) + Python FastAPI (<code>bluppi-audio-api:8000</code>)</p>
</li>
<li><p><strong>Infrastructure</strong>: Docker Compose</p>
</li>
<li><p><strong>Tunnel</strong>: Cloudflare Tunnel (<code>cloudflared</code>)</p>
</li>
<li><p><strong>Client</strong>: Flutter (Android/iOS)</p>
</li>
<li><p><strong>Domain</strong>: <code>bluppi.saikat.in</code></p>
</li>
</ul>
<p>What should have been a simple "expose my API to the internet" turned into a 6-hour debugging session. Here's everything I learned.</p>
<img src="https://cdn.hashnode.com/uploads/covers/67093bfb638dddf76481cbb9/fa59eda8-a56c-41a8-a787-e9781ac23d6a.png" alt="" style="display:block;margin:0 auto" />

<h2>Part 1: Setting Up Cloudflare Tunnel</h2>
<h3>Create the tunnel</h3>
<pre><code class="language-bash"># Login to Cloudflare
cloudflared tunnel login

# Create tunnel
cloudflared tunnel create bluppi

# Route your domain to the tunnel
cloudflared tunnel route dns bluppi bluppi.saikat.in

# Copy credentials to project
mkdir -p cloudflared
cp ~/.cloudflared/&lt;tunnel-id&gt;.json cloudflared/
cp ~/.cloudflared/cert.pem cloudflared/
</code></pre>
<h3>Initial <code>config.yml</code> (what I started with)</h3>
<pre><code class="language-yaml">tunnel: &lt;tunnel-id&gt;
credentials-file: /etc/cloudflared/&lt;tunnel-id&gt;.json

ingress:
  - hostname: bluppi.saikat.in
    path: /api/v2/.*
    service: grpc://bluppi-gateway:50050

  - hostname: bluppi.saikat.in
    path: /api/rest/.*
    service: http://bluppi-audio-api:8000

  - hostname: bluppi.saikat.in
    service: grpc://bluppi-api:50051

  - service: http_status:404
</code></pre>
<h3>Initial <code>docker-compose.yml</code> tunnel service</h3>
<pre><code class="language-yaml">tunnel:
  container_name: bluppi-tunnel
  image: cloudflare/cloudflared:latest
  command: tunnel --config /etc/cloudflared/config.yml run
  restart: unless-stopped
  networks:
    - cloudflared
  depends_on:
    - bluppi-api
    - bluppi-gateway
    - audio-api
  volumes:
    - ./cloudflared:/etc/cloudflared:ro
</code></pre>
<hr />
<h2>Part 2: The Mistakes (And How to Fix Them)</h2>
<h3>Mistake 1: Port 7844 blocked (college network)</h3>
<p><strong>Symptom:</strong></p>
<pre><code class="language-plaintext">dial tcp 198.41.192.107:7844: i/o timeout
</code></pre>
<p>cloudflared defaults to connecting Cloudflare's edge on TCP port <strong>7844</strong>. College networks often block non-standard ports.</p>
<p><strong>Fix:</strong> Force HTTP/2 protocol which falls back to port 443:</p>
<pre><code class="language-yaml"># docker-compose.yml
command: tunnel --protocol http2 --config /etc/cloudflared/config.yml run
</code></pre>
<p>Or in <code>config.yml</code>:</p>
<pre><code class="language-yaml">protocol: http2
</code></pre>
<hr />
<h3>Mistake 2: Too many HA connections</h3>
<p><strong>Symptom:</strong></p>
<pre><code class="language-plaintext">already connected to this server, trying another address
</code></pre>
<p>cloudflared opens <strong>4 parallel connections</strong> by default, but the Delhi edge (<code>del03</code>) only had 1-2 distinct IPs reachable from my network.</p>
<p><strong>Fix:</strong> Reduce HA connections:</p>
<pre><code class="language-yaml">ha-connections: 1
</code></pre>
<hr />
<h3>Mistake 3: <code>grpc://</code> is not a valid cloudflared service protocol</h3>
<p><strong>Symptom:</strong></p>
<pre><code class="language-plaintext">malformed HTTP response "\x00\x00\x06\x04..."
</code></pre>
<p><code>grpc://</code> is not a recognised protocol in <code>cloudflared</code>. Those bytes are raw HTTP/2 frames being received by cloudflared which expected HTTP/1.x.</p>
<p><strong>Fix:</strong> Initially, I changed <code>grpc://</code> to <code>h2c://</code> (HTTP/2 cleartext) for internal gRPC services. <strong>However, this is a trap.</strong> While <code>h2c://</code> stops this immediate error, it leads you right into Mistake 4 because of Cloudflare's proxy limitations.</p>
<hr />
<h3>Mistake 4: Cloudflare Free plan blocks gRPC on public hostnames</h3>
<p><strong>Symptom:</strong></p>
<pre><code class="language-plaintext">malformed header: missing HTTP content-type
</code></pre>
<p>Even with the gRPC toggle enabled in the Cloudflare Dashboard, the <strong>Free plan</strong> does not fully proxy gRPC through orange-cloud (proxied) hostnames. Cloudflare strips the <code>content-type: application/grpc</code> header.</p>
<p><strong>References:</strong></p>
<ul>
<li><p><a href="https://github.com/cloudflare/cloudflared/issues/491">cloudflared issue #491 — gRPC support</a></p>
</li>
<li><p><a href="https://github.com/cloudflare/cloudflared/issues/1304">cloudflared issue #1304 — Support h2c origin servers</a></p>
</li>
</ul>
<p><strong>Fix:</strong> This is a fundamental limitation, no simple config change fixes it. You cannot use <code>h2c://</code>. The real solution is adding TLS to your origin server and using <code>https://</code> (Detailed in Part 3).</p>
<hr />
<h2>Part 3: The Real Fix (TLS on the Origin)</h2>
<h3>Step 1: Generate self-signed certificates</h3>
<pre><code class="language-bash">mkdir -p certs
openssl req -x509 -newkey rsa:4096 \
  -keyout certs/server.key \
  -out certs/server.crt \
  -days 365 -nodes \
  -subj "/CN=localhost"
</code></pre>
<blockquote>
<p><strong>Note on CN:</strong> Since we use <code>noTLSVerify: true</code> in cloudflared config, the CN value doesn't matter. <code>localhost</code> works fine.</p>
</blockquote>
<h3>Step 2: Configure your gRPC Server</h3>
<p>Regardless of whether you are using Go, Python, Node, or Rust, you must configure your gRPC server to use the generated <code>server.crt</code> and <code>server.key</code>. Your specific language's gRPC library will handle the TLS implementation.</p>
<h3>Step 3: Verify TLS + ALPN on your server</h3>
<pre><code class="language-bash"># Check TLS works
openssl s_client -connect localhost:50051

# Check ALPN h2 is advertised (critical for gRPC)
openssl s_client -connect localhost:50051 -alpn h2
</code></pre>
<p>You must see:</p>
<pre><code class="language-plaintext">ALPN protocol: h2
</code></pre>
<p>If you see <code>No ALPN negotiated</code>, gRPC will not work through the tunnel. Check your server's TLS configuration.</p>
<blockquote>
<p><strong>Key point:</strong> <code>grpc-go</code> automatically advertises <code>ALPN: h2</code> when you use <code>credentials.NewServerTLSFromFile</code>. This is what enables cloudflared to negotiate HTTP/2.</p>
</blockquote>
<hr />
<h2>Part 4: Final Working Configuration</h2>
<h3><code>docker-compose.yml</code></h3>
<p>Since <code>cloudflared</code> is running on the host, we just need to make sure our backend services expose their ports to <code>localhost</code>.</p>
<pre><code class="language-yaml">  tunnel:
    container_name: bluppi-tunnel
    image: cloudflare/cloudflared:latest
    command: tunnel --no-autoupdate --protocol http2 --ha-connections 1 --config /etc/cloudflared/config.yml run
    restart: unless-stopped
    networks:
      - cloudflared
    depends_on:
      - bluppi-api
      - bluppi-gateway
      - audio-api
    volumes:
      - ./cloudflared:/etc/cloudflared:ro

networks:
  cloudflared:
    name: cloudflared
</code></pre>
<h3><code>cloudflared/config.yml</code></h3>
<pre><code class="language-yaml">tunnel: &lt;tunnel-token&gt;
credentials-file: /etc/cloudflared/&lt;tunnel-id&gt;.json
ha-connections: 1
protocol: http2

ingress:
  # 1. Dedicated REST API
  - hostname: bluppi.saikat.in
    path: /api/rest/.*
    service: http://bluppi-audio-api:8000

  # 2. gRPC Gateway
  - hostname: bluppi.saikat.in
    path: /api/v2/.*
    service: https://bluppi-gateway:50050
    originRequest:
      noTLSVerify: true
      http2Origin: true

  # 3. Pure gRPC API (Catch-All)
  - hostname: bluppi.saikat.in
    service: https://bluppi-api:50051
    originRequest:
      noTLSVerify: true
      http2Origin: true

  - service: http_status:404
</code></pre>
<p><strong>Why</strong> <code>https://</code> <strong>not</strong> <code>h2c://</code><strong>?</strong><br /><code>cloudflared</code> does NOT support <code>h2c://</code> (HTTP/2 cleartext) as an origin protocol — see <a href="https://github.com/cloudflare/cloudflared/issues/1304">issue #1304</a>. You must use <code>https://</code> with a TLS-enabled origin, then add <code>noTLSVerify: true</code> to accept the self-signed cert, and <code>http2Origin: true</code> to force HTTP/2 negotiation.</p>
<hr />
<h2>Part 6: Verification</h2>
<h3>Test gRPC end-to-end with grpcurl</h3>
<pre><code class="language-bash"># Install grpcurl (or you can use Postman)

# Test locally (bypassing tunnel) — baseline
grpcurl -plaintext localhost:50051 list
# Expected: Unauthenticated (server is running, auth is rejecting unauthenticated requests) (as I have setup JWT auth)

# Test through Cloudflare tunnel
grpcurl bluppi.saikat.in:443 list
# Expected: same Unauthenticated error = tunnel is fully transparent ✅
</code></pre>
<hr />
<h2>Part 7: Complete Mistake Summary</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Symptom</th>
<th>Root Cause</th>
<th>Fix</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><code>dial tcp ...:7844: i/o timeout</code></td>
<td>College network blocks port 7844</td>
<td><code>--protocol http2</code></td>
</tr>
<tr>
<td>2</td>
<td><code>already connected to this server</code></td>
<td>Del03 has limited edge IPs, 4 connections clash</td>
<td><code>ha-connections: 1</code></td>
</tr>
<tr>
<td>3</td>
<td><code>malformed HTTP response \x00\x00\x06\x04</code></td>
<td><code>grpc://</code> is not valid in cloudflared</td>
<td>Use <code>h2c://</code></td>
</tr>
<tr>
<td>4</td>
<td><code>use of closed network connection</code> / <code>EOF</code></td>
<td>cloudflared uses HTTP/1.1 to origin by default</td>
<td><code>http2Origin: true</code></td>
</tr>
<tr>
<td>5</td>
<td><code>No ALPN negotiated</code></td>
<td>gRPC server not advertising h2 during TLS handshake</td>
<td>Use <code>credentials.NewServerTLSFromFile</code> in Go</td>
</tr>
<tr>
<td>6</td>
<td><code>QUIC timeout</code> on <code>--token</code> mode</td>
<td>cloudflared token mode defaults to QUIC (UDP), also blocked</td>
<td><code>--protocol http2</code> flag</td>
</tr>
</tbody></table>
<hr />
<h2>Key Takeaways</h2>
<ol>
<li><p><code>grpc://</code> <strong>is not a valid cloudflared service protocol</strong> — use <code>h2c://</code> or <code>https://</code></p>
</li>
<li><p><strong>Cloudflare Free plan cannot proxy gRPC on public hostnames</strong> — cloudflare's gRPC toggle requires Pro plan to work properly</p>
</li>
<li><p><strong>cloudflared connects to origins using h2 (TLS), not h2c (cleartext)</strong> — your gRPC origin MUST serve TLS, even behind a private Docker network</p>
</li>
<li><p><code>http2Origin: true</code> <strong>is mandatory</strong> — without it, cloudflared uses HTTP/1.1 to the origin, which gRPC servers reject immediately</p>
</li>
<li><p><code>noTLSVerify: true</code> <strong>is safe inside Docker</strong> — the tunnel itself provides encryption; internal self-signed certs are fine</p>
</li>
<li><p><strong>Always test with</strong> <code>grpcurl</code> <strong>before testing Flutter</strong> — if <code>grpcurl bluppi.saikat.in:443 list</code> returns <code>Unauthenticated</code> (same as localhost), your Flutter app will work</p>
</li>
<li><p><strong>College/restricted networks block UDP and non-standard ports</strong> — always force <code>--protocol http2</code> to use TCP 443</p>
</li>
</ol>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://github.com/cloudflare/cloudflared/issues/491">cloudflared issue #491 — gRPC support request</a></p>
</li>
<li><p><a href="https://github.com/cloudflare/cloudflared/issues/1304">cloudflared issue #1304 — Support h2c origin servers</a></p>
</li>
<li><p><a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/">Cloudflare Docs — Cloudflare Tunnel</a></p>
</li>
</ul>
<hr />
<p><em>Written after 6 hours of debugging. Hopefully this saves you the same pain.</em></p>
]]></content:encoded></item><item><title><![CDATA[Firebase Draining Your Wallet? Here's How to Stop Paying for Every Single Read.]]></title><description><![CDATA[❗
In this blog, we’ll focus on using Dart/Flutter for our code examples, but keep in mind that you can apply these methods in any programming language. The key here is to focus on the techniques, not ]]></description><link>https://blog.saikat.in/firebase-draining-your-wallet-here-s-how-to-stop-paying-for-every-single-read</link><guid isPermaLink="true">https://blog.saikat.in/firebase-draining-your-wallet-here-s-how-to-stop-paying-for-every-single-read</guid><category><![CDATA[Firebase]]></category><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Saikat Das]]></dc:creator><pubDate>Sat, 14 Mar 2026 22:46:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734680734208/641c02b8-81c2-47aa-84ca-c7100eb23a41.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div>
<div>❗</div>
<div>In this blog, we’ll focus on using Dart/Flutter for our code examples, but keep in mind that you can apply these methods in any programming language. The key here is to focus on the techniques, not just the language!</div>
</div>

<p>If you're here, you probably already know that Firebase is great for building apps with real-time data. However, many developers run into a big issue—Firebase costs can quickly get out of hand due to too many read operations.</p>
<p>In 2018, a startup in Colombia faced this exact problem when they scaled up to 2 million daily active users (DAUs). A small, but costly mistake in their code ended up leaving them with a <strong>$30,356.56</strong> bill from Google Clouds in just 72 hours. Why? Because that tiny error caused 2 million users to each trigger 16,000 document reads, leading to over 40 billion requests to Firestore in less than 48 hours. <a href="https://hackernoon.com/how-we-spent-30k-usd-in-firebase-in-less-than-72-hours-307490bd24d"><em>Read the full article</em></a></p>
<p>You can avoid such excessive costs by optimizing your reads. In this blog, we'll dive into specific strategies to stop paying for every single read without sacrificing app performance.</p>
<h3>Understanding Firebase Read Costs</h3>
<p>Firebase's pricing model is simple. In Firestore, you are charged based on the number of documents you read. Each time a document is read from the database—whether through a query, real-time listener, or a simple fetch—it counts as a read.</p>
<p>Check out the <a href="https://cloud.google.com/firestore/pricing#firestore-pricing">Firestore Pricing</a> here.</p>
<p>Before jumping into solutions, it’s important to understand where things can go wrong.</p>
<ul>
<li><p><strong>Fetching unnecessary data:</strong> For instance, if you’re retrieving large collections when you only need a few documents, you’re paying for extra reads.</p>
</li>
<li><p><strong>Inefficient queries</strong>: Poor query structuring can result in pulling unnecessary data or performing repeated reads.</p>
</li>
<li><p><strong>Overuse of real-time syncing</strong>: Real-time listeners are great, but they can result in multiple reads as they sync data even when it’s not needed immediately.</p>
</li>
</ul>
<hr />
<h2>Strategies to Reduce Firebase Read Costs</h2>
<h3>Optimize Your Data Structure</h3>
<p>A well-organized database can greatly cut down the number of reads. If your Firestore database isn't structured efficiently, it can cause you to fetch more data than needed, leading to extra read costs.</p>
<p>Choosing when to use nested data vs flattened data is crucial for reducing unnecessary reads in Firestore.</p>
<table>
<thead>
<tr>
<th><strong>Nested Data</strong></th>
<th><strong>Flattened Data</strong></th>
</tr>
</thead>
<tbody><tr>
<td>If you often need to access both parent and child data at the same time, using a nested structure lets you fetch them with just one read.</td>
<td>Use this structure when you expect many related items, such as comments, tasks, or messages, that can quickly exceed document size limits.</td>
</tr>
</tbody></table>
<div>
<div>💡</div>
<div>Split data into multiple collections to avoid reading large, unnecessary datasets.</div>
</div>

<p><strong>Example</strong>: Instead of storing all user posts in one collection, divide them into smaller subcollections by category or user ID.</p>
<h3>Query Optimization</h3>
<p>Firestore is a NoSQL database, and it's important to use its indexing features. Writing efficient queries can help lower the number of documents read.</p>
<p>Firestore automatically indexes each document by its document ID. But for complex queries, like filtering on several fields, you might need to create composite indexes.</p>
<p><strong>Example</strong>: If you want to query users by both age and city, you'll need a composite index for <code>age</code> and <code>city</code>.</p>
<blockquote>
<p>Filtering documents with <code>where()</code> clauses retrieves only the necessary data.</p>
</blockquote>
<pre><code class="language-dart">final QuerySnapshot querySnapshot = await FirebaseFirestore.instance
      .collection('users')
      .where('age', isGreaterThanOrEqualTo: 18)
      .where('city', isEqualTo: 'New York')
      .get();
</code></pre>
<p><em>Make sure to combine filters that make sense together to reduce the number of reads.</em></p>
<blockquote>
<p><strong>Implement Pagination Using</strong> <code>limit()</code> for Efficient Data Retrieval</p>
</blockquote>
<pre><code class="language-dart">final int pageSize = 10;
DocumentSnapshot? lastVisible;

Future&lt;void&gt; getPosts() async {
  final query = FirebaseFirestore.instance
      .collection('posts')
      .orderBy('createdAt')
      .limit(pageSize)
      .startAfterDocument(lastVisible!);
  
  final querySnapshot = await query.get();

  querySnapshot.docs.forEach((doc) =&gt; print(doc.data()));
  lastVisible = querySnapshot.docs.isNotEmpty ? querySnapshot.docs.last : null;
}
</code></pre>
<p>Limiting documents can significantly reduce costs and improve performance, especially when dealing with large datasets.</p>
<div>
<div>💡</div>
<div>Full collection scans occur when you don't use indexes, making Firestore read every document in a collection. To avoid this, always use indexed fields in your queries.</div>
</div>

<h3>Avoid Unnecessary Real-time Listeners</h3>
<p>Real-time listeners are powerful but expensive if used without careful consideration. In many cases, you don't need real-time updates for every piece of data.</p>
<p><strong>Manual Refreshing:</strong> Instead of relying on real-time updates, users can manually trigger a refresh to load the latest data.</p>
<div>
<div>💡</div>
<div>Firestore offers offline persistence, enabling data to be cached on the client and synchronized later when the app reconnects. This feature can significantly reduce the number of reads, minimizing network calls during offline usage.</div>
</div>

<p>In this blog, we will look at advanced caching methods that can greatly reduce your Firestore costs. These strategies are more than just basic offline caching and can save you a lot of money while improving your app's performance.</p>
<hr />
<h1>Advanced Strategies to Reduce Firebase Costs</h1>
<p>You can use <code>SharedPreferences</code> to store small pieces of data that are frequently accessed, like user info or settings. This works well for data that doesn’t change much. However, <code>SharedPreferences</code> is only suitable for small amounts of data. If you store too much, it can slow down your app, use more memory, and hit storage limits.</p>
<pre><code class="language-dart">import 'package:shared_preferences/shared_preferences.dart';

Future&lt;void&gt; saveUserInfo(String name, int age, String email) async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  await prefs.setString('userName', name);
  await prefs.setInt('userAge', age);
  await prefs.setString('userEmail', email);
}
</code></pre>
<p>For example, you can store user information in SharedPreferences as small chunks of data that are repeatedly used throughout your application.</p>
<h3>Use Provider for efficient state management to reduce Firestore reads.</h3>
<p>Without proper state management, your app might request the same data from Firestore multiple times, increasing the number of reads unnecessarily. By using Provider, once data is fetched from Firestore, it can be stored and accessed efficiently throughout the app without additional Firestore calls.</p>
<pre><code class="language-dart">import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class UserProvider extends ChangeNotifier {
  Map&lt;String, dynamic&gt;? _userData;
  Map&lt;String, dynamic&gt;? get userData =&gt; _userData;

  Future&lt;void&gt; fetchUserData(String userId) async {
    if (_userData != null) return; // Skip fetching if data is already loaded

    try {
      final doc = await FirebaseFirestore.instance.collection('users').doc(userId).get();
      _userData = doc.data();
    } catch (e) {
      print('Error fetching user data: $e');
    } 
      
    notifyListeners();
  }
}
</code></pre>
<p>Don’t forget to add the Provider class to the <code>main.dart</code> file and then use it in the UI. This ensures the data is fetched only once when the app starts and is shared efficiently across all UI components, screens, and pages.</p>
<p>If you know that the data is not going to change frequently, you can save it on the client side, essentially caching the data. This can be achieved through various methods, such as using <code>Hive</code>, <code>SQLite</code>, or <code>path_provider</code>, each serving a different purpose.</p>
<table>
<thead>
<tr>
<th><strong>Hive</strong></th>
<th><strong>SQLite</strong></th>
<th><strong>path_provider</strong></th>
</tr>
</thead>
<tbody><tr>
<td>NoSQL, key-value pairs</td>
<td>Relational (SQL queries)</td>
<td>File system access (no database)</td>
</tr>
<tr>
<td>Simple setup, fast for small data</td>
<td>Complex setup, powerful for large datasets</td>
<td>Minimal setup, no querying or relations</td>
</tr>
</tbody></table>
<div>
<div>💡</div>
<div>Although <strong>path_provider</strong> is easy to set up, it is not efficient for storing JSON data, as it requires serializing and de-serializing the data into your model, which can be a heavy task. Therefore, <strong>Hive</strong> is often used instead.</div>
</div>

<hr />
<h2>Server-Side Optimization for Cost Reduction</h2>
<p>If all the users are requesting the same data from your Firebase database, it can unnecessarily increase your reads. To avoid this, you can use the Firebase Admin SDK to create bundles, request the data only once, store it in those bundles, and send the bundles to the clients. This will reduce Firebase reads and improve your app's performance, as it only needs to request the data once.</p>
<pre><code class="language-python">from google.cloud import firestore
from google.cloud.firestore_bundle import FirestoreBundle

db = firestore.Client()
bundle = FirestoreBundle("bundle-name")

for user in db.collection("users").stream():
    bundle.add_document(user.get())
    
    for post in db.collection("posts").where("user_id", "==", user.id).stream():
        bundle.add_document(post.get())

bundle_buffer = bundle.build()

with open("cacheBundle.bin", "wb") as file:
    file.wrtie(bundle_buffer)
</code></pre>
<p>The above code snippet might not be entirely correct as I forgot the exact syntax, but it was something similar to this. <a href="https://firebase.google.com/docs/firestore/bundles#python">Check Docs</a></p>
<p>Once you've created the Firestore bundle and serialized it, you can <strong>save the bundle as a file (e.g.,</strong> <code>cacheBundle.bin</code>) and send it to all of your clients via HTTP or upload it to <strong>Firebase Cloud Storage</strong>. This way, clients can access the cached data from the bundle without repeatedly making requests to Firebase Database.</p>
<h2>Conclusion</h2>
<p>Optimizing Firebase reads can help you save costs and improve app performance. By structuring your data efficiently, optimizing queries, minimizing real-time listeners, and using caching strategies, you can reduce unnecessary reads without sacrificing performance.</p>
<p>Feel free to share your own methods or any corrections in the comments below. Happy coding, and may your Firebase costs stay low while your app thrives!</p>
]]></content:encoded></item></channel></rss>