◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/Dev Tools
⟐ Dev Tools/2026-05-24Advanced

Running Streamlit + Gemini as a Production BI Dashboard — Auth, Cost, Caching, Rate Limits, Observability

A design memo for promoting a Streamlit + Gemini data analysis app into a real multi-user internal BI dashboard — authentication, cost optimization, result caching, per-user rate limits, and observability, all from production experience.

StreamlitGemini API249Data Analysis3BIAuthenticationProduction33

✦ Premium Article

I have been shipping personal apps since 2014, and as the indie developer behind 50 million cumulative downloads across a handful of mobile apps, I find myself reaching for Streamlit + Gemini more and more whenever I need a small internal dashboard — even though the "team" is just me. Watching AdMob revenue across six wallpaper apps, scanning App Store and Google Play reviews, eyeballing publishing throughput across two blogs (Lacrima and Mystery) — the first Streamlit version of that BI broke in roughly three weeks.

What broke: no login meant anyone with the URL could see the data; the Gemini bill quietly climbed past ¥4,000 a month on queries no one even read; the same CSV uploaded twice burned the API twice; and when something errored, I learned about it a week later. A Streamlit script you write for a personal experiment and a Streamlit BI you actually rely on share maybe 30–40% of the code. This article is the design memo for the rewritten side, based on the stack I actually run.

Why personal-experiment Streamlit and production BI are different beasts

Five layers that exist in the latter and not the former:

  1. Auth layer — who can access. A bare Streamlit app has none.
  2. Cost layer — something to break the linear relationship between user clicks and Gemini billing.
  3. Cache layer — never pay twice for the same question.
  4. Rate-limit layer — Gemini's project quota is one thing, per-user throttling is another.
  5. Observability layer — being able to ask later "who ran what, how many tokens did it burn, did we hit cache".

Personal experiments survive without any of the five. The moment a teammate or even a second device of yours starts hitting the URL daily, all five become non-negotiable.

The auth layer: streamlit-authenticator vs Auth0 vs Cloudflare Access

For the kind of indie BI I build, the team is 1–5 people. At that scale the realistic options collapse to three.

streamlit-authenticator (under 5 users, same-day deploy)

A Python package where you point a YAML file at hashed passwords and you get a login screen. This is what I started with. No additional infra, deployable to Streamlit Community Cloud in minutes.

# auth.py
import streamlit_authenticator as stauth
import yaml
from yaml.loader import SafeLoader
import streamlit as st
 
def get_authenticator():
    config = yaml.load(st.secrets["AUTH_CONFIG"], Loader=SafeLoader)
    return stauth.Authenticate(
        config["credentials"],
        config["cookie"]["name"],
        config["cookie"]["key"],
        config["cookie"]["expiry_days"],
    )
 
def login_gate():
    auth = get_authenticator()
    auth.login(location="main")
    if st.session_state.get("authentication_status") is not True:
        st.warning("Login required")
        st.stop()
    return st.session_state["username"]

Three gotchas you'll hit. First, expiry_days=30 gives long-lived cookie sessions with no real revocation; I keep mine at 7 and rotate hashes weekly via cron. Second, if that YAML ever lands in a public repo, you're done — keep it in Streamlit Secrets or an external KV. Third, the package does not provide MFA, so it's a poor fit for sensitive data.

Auth0 (10–30 users, MFA required)

When the headcount climbs and you need to revoke access cleanly, Auth0 is the next step up. The free tier covers 25,000 MAU, so an indie BI runs at zero cost. About 30 lines with Authlib:

# auth_oidc.py
from authlib.integrations.requests_client import OAuth2Session
import streamlit as st
 
AUTH0_DOMAIN = st.secrets["AUTH0_DOMAIN"]
CLIENT_ID = st.secrets["AUTH0_CLIENT_ID"]
CLIENT_SECRET = st.secrets["AUTH0_CLIENT_SECRET"]
REDIRECT_URI = st.secrets["AUTH0_REDIRECT_URI"]
 
def login_gate():
    if "user" in st.session_state:
        return st.session_state["user"]
 
    code = st.query_params.get("code")
    if code is None:
        sess = OAuth2Session(CLIENT_ID, CLIENT_SECRET, redirect_uri=REDIRECT_URI)
        url, _ = sess.create_authorization_url(
            f"https://{AUTH0_DOMAIN}/authorize", scope="openid profile email"
        )
        st.markdown(f"[Sign in]({url})")
        st.stop()
 
    sess = OAuth2Session(CLIENT_ID, CLIENT_SECRET, redirect_uri=REDIRECT_URI)
    token = sess.fetch_token(f"https://{AUTH0_DOMAIN}/oauth/token", code=code)
    user = sess.get(f"https://{AUTH0_DOMAIN}/userinfo").json()
    st.session_state["user"] = user
    st.query_params.clear()
    return user

If you go this route, always set up a Rule to restrict allowed email domains. Forget it and Auth0 will happily accept open signups from anywhere.

Cloudflare Access (my current default for indie BI)

The pattern I have settled on. Cloudflare Access does authentication at the edge, so the Streamlit code stays almost auth-free. You publish Streamlit via Cloud Run or a Cloudflare Tunnel, register the host as a Zero Trust Application, and Cloudflare authenticates with Google one-time PIN or GitHub before any request reaches your origin.

Three reasons I prefer it. First, auth lives outside Streamlit, so Streamlit upgrades cannot break login. Second, the free tier covers 50 users — generously beyond indie scale. Third, every request carries a Cf-Access-Authenticated-User-Email header, which makes user identification trivial.

# auth_cf.py
import streamlit as st
 
def login_gate():
    headers = st.context.headers if hasattr(st, "context") else {}
    email = headers.get("Cf-Access-Authenticated-User-Email")
    if not email:
        st.error("Missing auth header — access via Cloudflare Access")
        st.stop()
    return email

I recommend streamlit-authenticator for under-5-person internal BI, Cloudflare Access for everything else. Auth0 fits when you must onboard external collaborators with IdPs beyond Google/GitHub.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Five layers that change between a personal experiment and a real internal BI — each with concrete code you can lift into your own app
✦Token pricing and cache hit rates measured on a working Gemini 2.5 Flash vs 2.5 Pro vs 3.x Pro stack, in JPY/USD
✦When to pick streamlit-authenticator vs Auth0 vs Cloudflare Access, with team-size thresholds and concrete gotchas
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⟐ Dev Tools2026-05-03
Build a CSV Insight App with Gemini API and Streamlit — A Production-Ready Dashboard with Auto-Insights and Visualization
A production-grade implementation guide for a Streamlit + Gemini API data analysis app. Upload a CSV, get auto-insights and visualizations in seconds. Covers schema inference, structured output, and real-world rate-limit handling.
⟐ Dev Tools2026-03-30
Firebase Genkit × Gemini API in Production — Design Choices and Gotchas from a Month of Indie Backend Work
Field notes from a month of running Firebase Genkit and Gemini API behind an indie app review-summarization backend. Flow and Tool design, Cloud Functions vs. Cloud Run, real cost and latency numbers, and seven undocumented gotchas.
⟐ Dev Tools2026-09-10
Matching name variants in a print catalog: normalize first, then ask Gemini for candidates
When the same artist appears under three spellings, asking Gemini whether two names are the same person quietly merges different generations. Here is the two-layer alternative — deterministic normalization first, model-generated candidates second — with working code and a cost estimate.