Speed up slow-loading Home Assistant dashboards on large instances. A lightweight reverse-proxy add-on that serves your real Lovelace dashboards but strips the entity WebSocket down to only what each dashboard uses — so kiosks, wall panels, tablets, and Fully Kiosk Browser displays load in a fraction of the time, with zero loss of fidelity.
Keywords: Home Assistant slow dashboard, Lovelace performance, kiosk / wall-panel
load time, large instance with thousands of entities, subscribe_entities / get_states
firehose, WebSocket optimization, fast HA dashboard, Mushroom & custom-card kiosk.
A reverse proxy that serves your real Home Assistant dashboards (real frontend,
real cards — Mushroom, button-card, everything) but strips the entity websocket so a
kiosk page only subscribes to the entities that dashboard actually uses. On a big
instance (thousands of entities) this cuts the startup get_states / subscribe_entities
firehose down to a few dozen entities, so dashboards load fast — with zero visual
approximation, because it is the real frontend.
Home Assistant's frontend subscribes to every entity in your instance at page load
(get_states + subscribe_entities with no filter), then streams every state change for
all of them over the WebSocket. On a small setup that's fine. On a large instance — a few
thousand entities, plus heavy custom cards (Mushroom, button-card, auto-entities,
mini-graph-card) — that startup firehose makes dashboards slow to load and sluggish to
interact with, which is especially painful on low-powered kiosks, wall tablets, and
fridge/door displays that just need to show a handful of entities.
HA has no built-in way to tell a single dashboard "only subscribe to the entities I actually render." This add-on adds exactly that, without rebuilding your dashboards or sacrificing fidelity: it's a transparent proxy in front of HA that auto-detects each dashboard's entities and trims the subscription at the source. You keep your real cards and themes; the page just stops downloading and tracking thousands of irrelevant entities.
The proxy passes all HTTP straight through to HA (frontend bundles, auth, registries,
lovelace config, custom-card resources — untouched). It intercepts only /api/websocket:
- rewrites
subscribe_entities(no filter) to includeentity_ids = <allowlist>, so HA streams only those entities; - filters the
get_statesresponse to the allowlist.
Everything else passes through unchanged, so the real frontend renders normally.
The allowlist is the union of the entities used by each configured dashboard
(computed from its lovelace/config: card tree walk + auto-entities expansion + a
scan for entity ids referenced inside templates), plus your always_forward and minus
your never_forward overrides.
auto-entities filter cards are expanded against your live entities and registries, the
same way HA's frontend resolves them:
| Filter key | Resolved how |
|---|---|
entity_id, domain |
matched directly |
area, label, device, integration |
via the HA registries (matches id or name) |
name |
against friendly_name |
group |
expands to the group's members |
template |
rendered through HA, then real entity ids are taken from the output |
state, attributes |
over-included — an entity that doesn't match right now is still forwarded, so the card shows it when it later does |
Every one of those accepts an exact id, a * glob (anchored), or a /regex/ (not
auto-anchored — supply your own ^/$), matching auto-entities' own behaviour. Entities are
also pulled in transitively through groups, so a card naming a group — or expanding one
client-side, like enhanced-shutter-card's show_group_members — gets its members too, even
though they appear nowhere in the dashboard config.
The allowlist recomputes live when you edit a dashboard or change area/label/device assignments — no add-on restart, and no kiosk reload either: when a rebuild adds entities, open dashboard connections are dropped so the frontend reconnects and picks them up on its own.
- HA → Settings → Add-ons → Add-on Store → ⋮ → Repositories, add:
https://github.com/GabrielGoldsteinAnidea/HA-Websocket-Stripper - Install WebSocket Stripper, open Configuration, and set
dashboardsto your own dashboards'url_pathvalues (Settings → Dashboards). It ships empty — until you set it, the add-on refuses/api/websocketand says so in the log, rather than silently serving the untrimmed firehose:dashboards: - kitchen-panel # <-- your dashboards' url_path values - hallway-kiosk always_forward: [] # e.g. ["/^sun\\./", "person.alex"] never_forward: [] # e.g. ["/_battery$/"] strip_entities: true
- Start it. Browse
http://<ha-host>:8099/<your-dashboard>. Point your kiosk browser there. To move it off8099(e.g. it collides with Zigbee2MQTT), set theportoption — because the add-on runshost_network: true, the Network tab can't remap it.
Tip — keep broad admin dashboards out of the
dashboardslist. The allowlist is the union of every listed dashboard, so a big admin/overview dashboard built on wideauto-entitiesfilters (integration:, whole-area:) can legitimately resolve to thousands of entities and erase most of the trimming benefit. List only the lean kiosk dashboards you actually serve; the trim is dramatic for those (dozens of entities) and pointless for a dashboard that shows most of the instance anyway.
No long-lived token needed in the add-on — it uses the add-on's SUPERVISOR_TOKEN to
read the dashboard configs.
For a wall panel / fridge kiosk you usually don't want a password prompt. HA's
trusted_networks
auth provider shows a "pick a user" screen (or auto-selects one) for clients on trusted
IPs. To make it work through this proxy, HA must see the real browser IP — and getting
that right has two subtle requirements, both handled by this add-on out of the box:
- The add-on runs with
host_network: true(built in). This is essential: with a mapped port, Docker rewrites every client to the gateway172.30.32.1before the proxy ever sees it, so the kiosk's real IP is lost and trusted login can never match. Host networking lets the add-on see the real browser IP. - The add-on forwards that IP via
X-Forwarded-For, normalized to plain IPv4 (built in). Node reports dual-stack clients as IPv4-mapped IPv6 (::ffff:192.168.1.50), which won't match an IPv4trusted_networkssubnet; the add-on strips that prefix for you, on both HTTP requests and websocket upgrades.
Because of host_network, HA sees the proxied request coming from the host itself, so
trust the host (not the add-on docker subnet) in your HA configuration.yaml:
http: use_x_forwarded_for: true trusted_proxies: - 127.0.0.1 - ::1 # - 192.168.1.2 # also add the host's own LAN IP if the add-on reaches HA via it homeassistant: auth_providers: - type: trusted_networks trusted_networks: - 192.168.1.0/24 # <-- your kiosk's LAN subnet allow_bypass_login: true # skip the form entirely where one user is unambiguous # optional: auto-select a user per IP instead of showing the picker # trusted_users: # 192.168.1.50: <user-id-from-Settings-People> - type: homeassistant # keep this, or you lose password login entirely
Then restart HA Core (use_x_forwarded_for and auth_providers are core-config
changes, not a YAML quick-reload).
i️ Note: because the proxy presents requests to HA from the host, anyone who can reach the add-on's port effectively gets trusted-network login. That's the point for a kiosk on a trusted LAN, but it does mean the trimmed dashboards are reachable without a password by anything on that network — size your
trusted_networksaccordingly.
🛠️ If
host_networkbreaks startup (the add-on can't resolve the internalhomeassistant/supervisorhostnames), set theha_base/allow_ws_urloptions to pin them to IPs, e.g.ha_base: http://192.168.1.2:8123.
Putting your own reverse proxy in front of the add-on works — useful for TLS termination and external access, and required for browser features that only work on a secure origin (mic input for Assist, for instance).
The add-on preserves the X-Forwarded-For chain rather than replacing it, so HA sees the
real browser IP through both hops and trusted_networks still matches the kiosk, not your
edge proxy. Make sure HA trusts every hop:
http: use_x_forwarded_for: true trusted_proxies: - 127.0.0.1 # the stripper (reaches HA from the host, via host_network) - ::1 - 192.168.1.5 # your Caddy / nginx host, if it's a different machine
Note: versions before 0.2.3 flattened that chain, which made HA reject every proxied request with 400 Bad Request (
Incorrect number of elements in X-Forward-Proto) while a direct connection to:8123worked fine. If you hit that, update.
cd websocket-stripper npm install HA_TOKEN="<long-lived-token>" \ HA_BASE="http://homeassistant.mgmt:8123" \ DASH_PATHS="kitchen-panel,hallway-kiosk" \ node ha_ws_trim_proxy.mjs # then open http://localhost:8099/kitchen-panel
Run npm test for the suite (extractor + registry/filter resolution unit tests, plus
integration tests that drive the real proxy against a mock HA).
Set STRIP_ENTITIES=0 to passthrough untrimmed for an A/B load comparison.
- The frontend JS bundles still load (and are cached after first visit); this targets the per-load entity firehose, which is the part that scales with instance size.
- The allowlist recomputes live on dashboard edits and registry changes, and open kiosk
pages reconnect themselves when it grows. Adding a whole new dashboard to the
dashboardsoption still needs an add-on restart (options are read at boot). - Each recompute logs the exact
+added/-removedentity diff, so you can see from the add-on log what a dashboard edit changed. - Restarting Home Assistant is safe. The add-on stays up and waits: HTTP degrades to 502 while core is down, then it reconnects, rebuilds, and open dashboards recover on their own. Same at host boot, when the add-on starts before core is listening.
- Cards can still show "unavailable" if a filter type isn't supported yet — currently
not,and,or,floor,device_manufacturer,device_model,last_changed. List those entities inalways_forwardand open an issue. - A local add-on bakes the code into its image at build time, so updates need a Rebuild, not a Restart.
- See
CLAUDE.mdfor architecture/decisions andwebsocket-stripper/DOCS.mdfor option details.
If this saved you an evening of debugging, a coffee is appreciated.
Full history in websocket-stripper/CHANGELOG.md.
- auto-entities globs and regexes work on every filter key —
/^sensor\.pv_.*_power$/and friends resolve instead of matching nothing, ondomain/area/label/device/integration/name, not justentity_id. filter: template:cards resolve — rendered through HA rather than skipped.- Group members are pulled in transitively, so cards that expand a group client-side
(
show_group_members) stop showing their members as "unavailable". - Fixed 400 Bad Request behind another reverse proxy — the
X-Forwarded-Forchain is preserved, so Caddy / nginx / Traefik in front of the add-on works. - Open dashboards pick up new entities by themselves — no more reloading every wall panel after a dashboard edit.
- An empty allowlist is never forwarded as "no filter" again. HA reads
set(msg["entity_ids"]) or None, so an emptyentity_idsmeant no filter at all — and becausedashboardsused to default to the author's own dashboards, a fresh install resolved nothing and relayed every entity on the instance, the exact opposite of the point.dashboardsnow defaults to[]and the websocket is refused (with the reason logged) rather than sending an empty filter. - Surviving an HA restart — the add-on no longer crash-loops when core goes away.
- auto-entities
area/label/device/integrationfilters resolve against the registries, so you don't have to hand-list them inalways_forward. - Configurable
portoption — coexist with other add-ons on busy hosts. - Recompute logs the
+added/-removedentity diff, not just the total. - Defensive egress filter re-filters
subscribe_entitiesevent payloads to the allowlist on the way to the browser — a belt-and-suspenders guarantee the firehose can't leak even if a future HA ignored the subscription filter. - Added a test suite (
cd websocket-stripper && npm test): unit tests for the extractor + registry resolver, and integration tests that run the real proxy against a mock HA.