Streamlit vs. NiceGUI (2026): Best Python Web Framework
Streamlit vs NiceGUI in 2026: compare the two Python web frameworks on architecture, auth, real-time updates, self-hosting, and which one fits your project.
25 min read

Streamlit vs NiceGUI is the comparison I see most often when someone wants a Python web framework and does not want to hand-write HTML. Both let you build interactive web apps in mostly Python, both are open source, and both run fine as a single Docker container on a small VPS. Both also changed enough in the last year that older comparison posts are now wrong.
Updated October 2026. Versions checked against PyPI and GitHub on 2026-10-07: Streamlit 1.65.0 (released 2026-10-02) and NiceGUI 3.17.1 (2026-09-18). If you read a guide that still calls NiceGUI a 2.0-era framework, close it.
The short positioning stays the same as in 2024. Streamlit targets data apps: dashboards, data exploration, ML demos. You write a Python script top to bottom and Streamlit renders it as a web app with built-in charts, tables, and filters. NiceGUI targets application-style UIs: forms, admin panels, monitoring tools, IoT controllers. It is event-driven, runs on FastAPI, and gives you far more control over layout, events, and state.
Quick comparison overview
| Feature | Streamlit 1.65 | NiceGUI 3.17 |
|---|---|---|
| Best for | Data apps, dashboards, ML demos | App-style UIs: forms, panels, monitoring |
| Authoring model | Script reruns (with fragments for partial updates) | Event-driven components over WebSockets |
| Underlying server | Starlette/Uvicorn (ASGI, custom routes via st.App) |
FastAPI/Starlette (mount into your own app) |
| Customization | Theming + custom components v2 | Full layout control, Quasar/Vue, Tailwind 4 |
| Built-in auth | st.login() / st.logout() (OIDC) |
Middleware + app.storage.user pattern |
| Python | >= 3.10 | >= 3.10, < 4 |
| License | Apache-2.0 (Snowflake-backed) | MIT (independent) |
This table is orientation, not the verdict. The decision still comes down to the developer model: script reruns versus event callbacks. Get that right and the rest follows.
For more options beyond these two, see: Best Python Web Frameworks
Streamlit vs NiceGUI: main differences in 2026
The core positioning has not moved. What changed is the plumbing under both frameworks. Here is how they differ in 2026, with the dates so you can see how fresh the claims are.
Primary purpose
Streamlit is built for data science, ML, and analytics dashboards. Its widget set, DataFrame rendering, and chart helpers all assume that workflow. NiceGUI is built for general-purpose web applications with desktop-like interfaces: forms, admin panels, control screens.
Neither framework has crossed over in 2026. Streamlit still feels like a notebook that grew a web server. NiceGUI still feels like a GUI toolkit that happens to run in the browser. NiceGUI also ships the extras that make it an app framework rather than a dashboard tool: native/desktop windows (PySide6-based), run.cpu_bound and run.io_bound for background work, and ui.timer for live dashboards.
Development model: script reruns vs event-driven
This is the decision factor. Everything else is secondary.
Streamlit runs your script top to bottom on every interaction. That model is dead simple until your app grows heavy work at the top of the file. Since 1.37 and with steady additions through 1.63, @st.fragment scopes a rerun to one block of the page (1.58 added parallel=True; 1.63 added event-scoped reruns with @st.fragment(key=...) and st.rerun("name")). Fragments fix the worst of the full-rerun jank, but the mental model is still “the script runs again”.
NiceGUI calls your handler and updates the elements you touch. UI state syncs over a persistent WebSocket. Nothing reruns from the top.
If your mental model is a notebook, Streamlit wins. If it is a GUI with event handlers, NiceGUI wins. Look back at the diagram above.
Customization
The old “Streamlit is limited” line needs an asterisk now. Streamlit 1.51 (2025-10-29) shipped custom components v2 (frameless, bidirectional), custom light and dark themes, and reusable theme files. You can build a branded internal dashboard without forking the framework.
It is still not close to NiceGUI. NiceGUI sits on Quasar/Vue with Tailwind 4 (upgraded in 3.0), and you can drop to raw HTML/CSS/JS whenever the component library runs out. Layouts are explicit: rows, columns, grids, tabs, dialogs, drawers. Multi-page apps are a first-class pattern via @ui.page (see how to add multiple pages to NiceGUI).
Streamlit has multi-page apps too, but navigation is file-based and the page chrome is mostly fixed. If you need pixel-level control or a layout that does not look like a Streamlit app, NiceGUI is the pick.
Backend architecture
Both frameworks are ASGI apps in 2026. The old “Streamlit has a toy server, NiceGUI has FastAPI” framing is dead.
Streamlit replaced Tornado with Starlette/Uvicorn in 1.57 (2026-04-29). st.App (experimental in 1.53, available with the Starlette switch) is an ASGI entry point that accepts custom routes, middleware, and lifecycle hooks, and plays with FastAPI or Starlette apps. Async/await in app code landed in 1.64 (2026-09-15), cached functions included.
NiceGUI has been FastAPI/Starlette/Uvicorn all along and can mount into an existing FastAPI app so the API and the UI share one process. The documented integration point is ui.run_with(app) (check the Configuration and deployment section of the NiceGUI docs for the exact signature on your version).
Both frameworks run on ASGI in 2026
The old “Streamlit has no real backend” argument is outdated. The differentiator is the authoring model, not the server underneath. Pick on how you want to write the app, not on which HTTP stack it ships with.
Real-time features
Streamlit updates on interaction: a widget change reruns the script (or the fragment). Since 2026 you can target reruns more precisely, but the model is still pull-on-interaction. Good enough for filters, drilldowns, and dashboards that refresh when a user clicks.
NiceGUI is the natural fit for monitoring and control panels. ui.timer polls or pushes, each client holds its own state, and handlers update individual elements. Live counters, log tails, IoT controls, and “the page updates without me touching it” UIs are the default behavior, not a workaround.
Learning curve
Streamlit is still the friendliest way into Python web apps. If you can write a script with pandas, you can ship a dashboard in an afternoon. You do not write HTML or callbacks, and there is no state machine to manage.
NiceGUI is slightly steeper. You need to understand event handlers, when to use ui.clear(), and how per-client state works. Basic HTML/CSS knowledge helps with layout. It pays that cost back the moment you build something with more than a few interactions.
Both now require Python >= 3.10 (PyPI requires_python, checked 2026-10-07). On an old base image, install fails before you write a line of code.
Community & resources
GitHub stars as of 2026-10-07: Streamlit 45,909, Gradio 43,677, Reflex 28,937, Dash 24,442, Flet 17,263, NiceGUI 16,269. Download counts and stars are not quality, but they do predict how fast you find a Stack Overflow answer.
Streamlit releases every 4 to 6 weeks and has a large example ecosystem, plus Streamlit Community Cloud for free publishing. NiceGUI is active (58 open issues at last check) with the 3.x line maturing toward a 4.0 that is already flagged in release notes. Its community is smaller and self-hosting-focused. You will read the docs more and Stack Overflow less.
License & cost
Streamlit is Apache-2.0 and has been owned by Snowflake since 2022. The OSS package stays Apache; “Streamlit in Snowflake” is the commercial path. NiceGUI is MIT, maintained by Zauberzeug (independent), with no paid tier.
Streamlit Community Cloud hibernates apps
Free Streamlit Community Cloud apps hibernate after 12 hours without traffic and wake on the next visit (official docs, retrieved 2026-10-07; older “7 days of inactivity” wording floating around the web is stale). The free tier wants a public repo and is resource-capped. That is fine for a demo and wrong for an internal tool with an SLA. I would not put anything a colleague depends on behind Community Cloud.
Authentication in 2026
The old advice was “Streamlit has no auth, NiceGUI needs FastAPI patterns”. Half of that is now wrong. Streamlit shipped first-party OIDC in 1.42 (2025-02-04) and st.user.tokens in 1.53 (2026-01-14). NiceGUI’s story is unchanged: you assemble auth yourself, and you own the session stack.
Streamlit’s built-in OIDC (st.login() / st.logout())
st.login() redirects to any OIDC provider you configure (Google, Okta, Auth0, Keycloak, whatever speaks OIDC). st.user gives you the claims after the redirect; st.user.tokens exposes the raw OIDC tokens if you need to call downstream APIs. Configuration lives in .streamlit/secrets.toml. The docs for the exact secrets shape have moved around, so copy from the current Streamlit auth page rather than from an older tutorial.
NiceGUI’s middleware + app.storage.user pattern
NiceGUI documents an Authentication pattern built on middleware, app.storage.user, and a storage_secret. You wire the login check yourself, store the user identity in per-user storage, and redirect unauthenticated clients. When you mount NiceGUI into your own FastAPI app, you can instead use whatever auth middleware or dependency injection you already run. More assembly required, but you own the session and auth stack end to end.
The two patterns as minimal code shapes:
Streamlit OIDC
import streamlit as st
if not st.user.is_logged_in:
st.login() # redirects to your OIDC provider
else:
st.write(f"Hello, {st.user.name}")
# st.user.tokens has the raw OIDC tokens (since 1.53)
st.button("Log out", on_click=st.logout)NiceGUI middleware
from nicegui import ui, app
# storage_secret comes from ui.run(storage_secret='change-me-to-a-long-random-string')
# and is required before app.storage.user works.
@ui.page('/private')
def private_page():
if not app.storage.user.get('authenticated', False):
ui.navigate.to('/login')
return
ui.label(f"Hello, {app.storage.user.get('username')}")
# Wire the actual login check in middleware (see NiceGUI docs,
# Authentication section) or use FastAPI auth when mounted via ui.run_with(app)When to choose each framework
“Streamlit vs NiceGUI which is better” is the wrong question. Which fits the project is the right one. Here is the split I use.
Choose Streamlit if…
Streamlit is the fastest path from notebook to shared dashboard.
- You need data dashboards with charts, tables, and filters, and the work is pandas or SQL shaped
- You are building ML model demos or internal prototypes and want them public today
- You want rapid prototyping with minimal code and no web development background
- Sharing matters more than polish (Streamlit Community Cloud gets a link out in minutes)
- You are wiring an AI or data tool and want the UI layer to stay boring: for example use Groq’s free API in your Streamlit app or build an AI article writer with Streamlit
- Deployment is a VPS away: deploy Streamlit on a VPS with Cloudflare Tunnels
Example use cases: sales analytics dashboard, machine learning model demo, financial data explorer, research data visualization.
Choose NiceGUI if…
NiceGUI is the tool when the “dashboard” grows buttons, forms, and users.
- You are building desktop-like applications in the browser (admin panels, tools, controllers)
- Real-time interactions without reloads are a requirement, not a nice-to-have
- You need custom layouts and components beyond a standard widget set
- You want APIs and UI in the same app, with explicit control over routing and auth
- Multiple users will operate the app, not just look at it
- You want a walk-through first: NiceGUI tutorial for beginners
Example use cases: project management tools, IoT device controllers, interactive forms and surveys, real-time monitoring systems.
NiceGUI also runs as a native desktop window (PySide6), which is a nice trick when the “web app” turns out to be an internal desktop tool.
When neither framework fits
Honest limits
Long-lived multi-user CRUD with complex permissions, strict role systems, or heavy relational data modeling belongs in Django, FastAPI with templates, Reflex, or a full-stack JS framework. Both Streamlit and NiceGUI will fight you on that shape of problem. And if you need a mobile app, neither of these is the answer. Pick the boring option and move on.
Getting started: code examples
Pin your versions
Both frameworks require Python >= 3.10 (if yours is older, see how to install and upgrade Python on Mac). Pin versions in requirements.txt and rebuild deliberately. NiceGUI 3.0 (2025-10-03) was breaking and 4.0 deprecations are already announced. Streamlit removes deprecated APIs aggressively (1.58 dropped element.add_rows and the LangChain callback handler). Do not pip install -U blindly on a production container. I manage Python projects with uv; here is how to set up a Python project with uv if you want the same workflow.
Streamlit example: data dashboard
import streamlit as st
import pandas as pd
st.title("Sales Dashboard")
data = pd.DataFrame({
"Month": ["Jan", "Feb", "Mar", "Apr"],
"Sales": [100, 150, 120, 200],
})
month = st.selectbox("Select month", data["Month"])
filtered = data[data["Month"] == month]
st.bar_chart(data.set_index("Month"))
st.metric(f"Sales for {month}", f"${int(filtered['Sales'].iloc[0])}")
@st.fragment # reruns only this block, not the whole script
def notes():
if st.button("Add note"):
st.session_state.notes = st.session_state.get("notes", []) + [month]
notes()Verify: install and run it, then check the app serves.
pip install streamlit pandas
streamlit run app.pyOpen http://localhost:8501. As a sanity check before you write anything, streamlit hello runs the built-in demo app. If curl -s http://localhost:8501/_stcore/health prints ok, the server is up.
Failure mode: Port 8501 is already in use means something else owns the port. Either stop it or run streamlit run app.py --server.port 8502.
NiceGUI example: interactive form
from nicegui import ui
def handle_submit():
ui.notify(f"Hello {name.value}! You are {age.value} years old.")
ui.label("User Information Form")
with ui.row():
name = ui.input("Name", placeholder="Enter your name")
age = ui.number("Age", value=25, min=0, max=120)
ui.button("Submit", on_click=handle_submit)
ui.run(title="bitdoze demo") # NiceGUI 3.x, Uvicorn under the hood, WebSocket UI syncVerify:
pip install nicegui
python main.pyOpen http://localhost:8080, submit the form, and confirm the notification toast appears in the bottom-right corner. The terminal shows the Uvicorn startup line; that is your hint the server is listening.
Failure mode: the most common install error is Requires-Python >=3.10 (or a resolution error mentioning it). The fix is a newer Python, not a newer pip.
Which is easier to learn?
Streamlit wins for absolute beginners. Linear script in, web app out. NiceGUI is accessible once the callback plus ui.clear() mental model clicks, and it keeps paying off as the app grows. Both need Python >= 3.10. If you have never built a web app, start with Streamlit for a weekend, then look at NiceGUI when the rerun model starts annoying you.
Self-hosting Streamlit and NiceGUI on a VPS
Both frameworks self-host the same boring way: one Docker container behind a reverse proxy on a small VPS. Neither needs a database to run. This is the section the other comparison posts skip, and it is where both frameworks fail in production.
Prerequisites
Before you start, have all of this in place:
- A Linux VPS with 2 vCPU and 2-4 GB RAM. Something like a Hetzner Cloud VPS (CX22 class, around EUR 5/month) runs both apps comfortably
- Docker and Docker Compose installed on the server
- A domain or LAN hostname pointing at the VPS
- Python >= 3.10 in the image you build from (python:3.12-slim is a safe default)
- A reverse proxy (Caddy, Traefik, or Nginx) or Dokploy with its bundled Traefik
- Knowledge that both apps are WebSocket-heavy: the proxy must forward the
UpgradeandConnectionheaders
Docker setup and healthchecks
A container without a healthcheck is a container you cannot restart safely. Streamlit has a built-in endpoint. NiceGUI does not, so you expose one.
Streamlit healthcheck
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
HEALTHCHECK CMD curl -f http://localhost:8501/_stcore/health || exit 1
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]NiceGUI healthcheck
# main.py
from fastapi import FastAPI
from nicegui import ui
app = FastAPI()
@app.get('/health')
def health():
return {'ok': True}
@ui.page('/')
def index():
ui.label('Hello from NiceGUI')
# Mount NiceGUI into the FastAPI app so /health and the UI share one process.
# Check the exact run_with signature in the NiceGUI docs for your version.
ui.run_with(app)FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]Streamlit has a real health endpoint
GET /_stcore/health returns ok and is the standard container healthcheck for Streamlit (Streamlit Knowledge Base). NiceGUI ships no built-in health route, which is why the FastAPI mount pattern above is worth the five extra lines. Double-check the run_with API name against the Configuration and deployment section of the NiceGUI docs for your pinned version before you bake it into an image.
A minimal compose file shape (trimmed to the moving parts):
services:
streamlit:
build: ./streamlit-app
ports:
- "8501:8501"
restart: unless-stopped
nicegui:
build: ./nicegui-app
ports:
- "8080:8080"
restart: unless-stoppedVerify:
curl -f http://localhost:8501/_stcore/health # -> ok
curl -f http://localhost:8080/health # -> {"ok": true}
docker ps # STATUS column shows (healthy)Failure modes: curl: (7) Connection refused inside the container usually means the app is still starting or bound to 127.0.0.1 instead of 0.0.0.0. curl: (22) from the healthcheck means the endpoint 404’d, which for NiceGUI means the FastAPI route is not mounted. unhealthy in docker ps with an app that works in the browser is almost always a health URL typo.
For a longer treatment of running Python apps in containers, see run any Python app in Docker with Docker Compose.
Reverse proxy and WebSocket notes
The number one real-world failure for both frameworks is a blank page or an endless spinner behind a reverse proxy. Both symptoms mean the same thing: the WebSocket upgrade is not being forwarded.
Nginx
location / {
proxy_pass http://streamlit:8501;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}Traefik
labels:
- traefik.http.routers.streamlit.rule=Host(`app.example.com`)
- traefik.http.services.streamlit.loadbalancer.server.port=8501
# Traefik forwards WebSocket upgrades by default.
# Raise forwardAuth/response timeouts if you see idle sockets drop.Caddy
app.example.com {
reverse_proxy streamlit:8501
# Caddy forwards WebSocket upgrades without extra config
}Verify: open the app through the proxy URL, not the container port. Browser dev tools, Network tab: the WebSocket request should show status 101 (Switching Protocols). A status 200 or 400 on that request is your upgrade failing.
Failure modes:
- Blank page, no errors in the app log: missing
Upgrade/Connectionheaders (Nginx without theproxy_http_version 1.1block above). - App loads, then spinner never resolves: an idle timeout is killing the socket. Raise
proxy_read_timeoutor the Traefik equivalent. - Streamlit 1.60 (2026-07-21) shipped breaking security hardening: origin and host-message checks (CWE-346), query-string caps at 512 KiB / 1,000 fields, and
server.maxWidgetStateSize(default 25 MB). If you run Streamlit behind a proxy with non-standardHostorX-Forwarded-Hosthandling, read that release note before upgrading and test on your own stack. A standard Caddy or Traefik setup should be unaffected, but unusual header rewriting is exactly what those checks look for.
Do not run old NiceGUI 3.x on a public endpoint
NiceGUI 3.16 and 3.17 shipped security fixes you want: a memory-exhaustion bug on socket disconnect (GHSA-46f2-xhpw-8vh3, Aug 2026), an XSS via prop injection (GHSA-955g-h32v-mvrr), and unguessable single-use ui.download routes (GHSA-p92q-2755-mhgh, Sep 2026). Upgrade to at least 3.17.1 before the app is reachable from the internet. This is also the argument for pinning versions: pin, then bump deliberately.
Cost and resource notes
Order-of-magnitude numbers, because this is where people get surprised:
- Both apps self-host on one small VPS (2 vCPU, 2-4 GB RAM, roughly EUR 5/month). Neither needs a database. A Hostinger VPS is a budget alternative with NVMe storage if you want more disk for data workloads.
- Streamlit holds state per session process. RAM scales with concurrent sessions, not just with the code. If twenty people open your dashboard, budget for twenty sessions. This is the real VPS sizing consideration.
- NiceGUI also holds per-client state in the worker. Multi-worker deployments have caveats (documented in the NiceGUI docs), so read that before scaling horizontally.
- Streamlit Community Cloud: free, but 12-hour hibernation plus resource caps, as covered above. Fine for demos. For always-on internal tools, self-host.
- Pin your image tags and package versions. NiceGUI 3.0 was breaking and 4.0 deprecations are already announced; Streamlit removes deprecated APIs every few releases.
- Rollback path: keep the previous image tag. If a bump breaks the app,
docker composeup the old tag. That is the whole undo plan, and it works.
If you want the PaaS-shaped route instead of raw Compose, deploy a Python uv project with Dokploy. For the full Streamlit runbook including TLS and tunnels, deploy Streamlit on a VPS with Cloudflare Tunnels.
Full Streamlit VPS deployment guideAlternatives in 2026
If neither framework fits, the current Streamlit alternatives (versions and stars checked 2026-10-07) look like this:
- Gradio 6 (6.29.1, 43.7k stars): ML demos and Hugging Face Spaces. If the app is “model in, sample out”, Gradio is still the shortest path.
- Dash 4.4 (24.4k stars): the OG Python analytics framework from Plotly, now on the 4.x line. Heavy-duty dashboards with callbacks.
- Reflex 0.9 (28.9k stars): full-stack Python that compiles to Next.js. More like building a traditional web app, just without leaving Python.
- Flet (17.3k stars): Flutter-based, ships desktop and web from one codebase.
- Taipy: pipeline plus GUI for data apps. We compared it directly in the Streamlit vs Taipy comparison.
- FastHTML: lightweight Python UI for people who want HTML-shaped control without a big framework. See FastHTML for beginners.
One cautionary tale: Google’s Mesop showed up on every “worth watching” list in 2024, including this article’s earlier version. The repo is archived in 2026. Experimental frameworks from big vendors can disappear. Favor projects with a release history you can audit.
The wider field is in best Python web frameworks.
Which should you choose?
To answer “Streamlit vs NiceGUI which is better” directly: neither is objectively better. Pick for the project, not for the benchmark chart. Both are actively maintained and fine for production in 2026.
The two one-liners
Streamlit is the fastest path from notebook to shared dashboard. NiceGUI is the tool when the “dashboard” grows buttons, forms, and users. If you can say which of those your project is, you already have your answer.
Start with the framework that fits the project in front of you. You can switch later if the project changes shape; both APIs are small enough that a rewrite after a prototype is cheap.
Streamlit vs NiceGUI FAQ
Is NiceGUI faster than Streamlit?
It is not a yes/no speed question, because the execution models differ. NiceGUI updates only the elements touched by the event handler. Streamlit reruns the script (or a fragment) and diffs the UI, so heavy top-of-file work hurts until you cache or fragment it. Measure on your workload, but for interaction-heavy UIs NiceGUI’s model does less redundant work by default.
Can Streamlit handle multi-page apps and forms?
Yes. Streamlit has multi-page apps, fragments, and session state, and you can build real forms with them. Where it strains is app-style UIs with many buttons, forms, and concurrent users: that is where NiceGUI’s event-driven model keeps the code simpler and the browser responsive.
Does NiceGUI work with FastAPI?
Yes. NiceGUI is built on FastAPI and can mount into an existing FastAPI app so your API routes and UI share one process. The documented integration point is ui.run_with(app); check the exact signature in the NiceGUI docs for your pinned version. You can also keep them separate and run NiceGUI standalone with ui.run().
Do I need Docker to self-host either framework?
No. streamlit run app.py and python main.py work fine on a bare VPS behind Nginx or Caddy. But one container plus a reverse proxy is the boring reliable path: reproducible builds, healthchecks, restart policies, and a one-line rollback to the previous image tag. If you want the container approach, see how to run any Python app in Docker with Docker Compose.
Is Streamlit free for commercial use?
Yes. Streamlit is Apache-2.0; you can build and sell commercial apps on it. The paid path is Streamlit in Snowflake, not the OSS package. Streamlit Community Cloud has a free tier with hibernation after 12 hours and resource caps. Self-hosting has no license cost for either Streamlit or NiceGUI (NiceGUI is MIT), just your server bill.
Quick start resources
Streamlit:
NiceGUI:
- Official documentation (the Authentication and Configuration/deployment sections back the auth and healthcheck patterns above)
- NiceGUI tutorial for beginners
- GitHub examples


