Fair enough, those are reasonable points. I left the numbers out to avoid drowning everyone, but you’re right that without them it’s just another “my HA is slow” thread. So here’s everything, method included.
Up front: I changed my mind twice while measuring. I first thought it was the size of my dashboard, then the per-request latency. Both were wrong. What follows is what’s left after ruling everything else out.
Test conditions
Mac mini on wired gigabit ethernet, no WiFi, no cellular. Free fibre line. HA Yellow (CM4), Core 2026.8.1, also on wired gigabit. Each pair of tests run a couple of minutes apart. I hit two addresses from the same machine: the local IP, and the Nabu Casa hostname. Worth noting, even from inside my own network that hostname resolves to the relay, so the traffic really does go out and come back through the tunnel. It’s not a local shortcut. Relay in question: eu-central-1-15.ui.nabu.casa, region eu-central-1.
The numbers below come from the Mac mini, on both Safari and Chrome — same results either way.
But the symptom itself shows up everywhere I’ve tried it: Safari and Chrome on the Mac mini, Safari and Chrome on iOS, Chrome on Android, and the Home Assistant companion app on both iOS and Android. So it isn’t a browser, a rendering engine, an OS, or a particular client-side cache.
Since you asked about my connection
Phone on 5G, outdoors, SFR: 659 Mb/s down, 43 Mb/s up, 11 to 21 ms latency.
Same phone on my home WiFi, Free fibre: 639 Mb/s down, 604 Mb/s up, 13 to 15 ms latency.
The upload from home is the one that matters, since that’s HA pushing data outwards. I’ll be honest about the method: a speedtest measures against a nearby test server, not against the relay, so it isn’t the same route. Keep that 604 Mb/s in mind anyway.
And the Yellow’s own link, before anyone asks
Gigabit ethernet, no WiFi. I had it serve the 3.5 MB of /api/states locally: 2 seconds, so about 15 Mb/s. On a 1000 Mb/s link the cable clearly isn’t working hard — what limits it is the CM4’s serialisation speed. If its link were the problem, local access would be slow too. It isn’t.
The raw stream, no dashboard involved
Same /api/states request, about 3.6 MB, same token, LAN versus the cloud URL:
- LAN: 3.6 MB in roughly 0.3 s, so ~90 Mb/s
- Cloud: 3.6 MB in 86 to 99 seconds, so ~0.3 Mb/s
No dashboard in there. No custom cards, no rendering, no WebSocket. Just a GET returning JSON. The ratio is already 300 to 1.
The test that identifies what kind of throttling this is
This is the one that made me drop the latency theory. I ran the same request over 1, then 2, then 4 parallel connections, capped at 2 MB each:
- 1 connection: 0.35 Mb/s
- 2 connections: 0.46 Mb/s total, so 0.23 each
- 4 connections: 0.38 Mb/s total, so 0.09 each
The aggregate stays flat. If this were a per-connection latency problem, four connections would have given roughly four times the throughput. Instead they share a fixed-size cake. So it’s an aggregate cap on the tunnel, somewhere around 0.4 Mb/s.
And it isn’t transient: I forced a disconnect and reconnect of the tunnel through cloud/remote/disconnect and connect. It lands on the same node and the throughput is unchanged. The cap is persistent and there’s nothing I can do about it from my side.
The test that definitively rules out my dashboard
I went all the way on this one, because it’s the objection I get every time. I backed up my config, then replaced it with this, and nothing else:
title: Aperçu
views:
- title: Hello
path: accueil
icon: mdi:home-heart
cards:
- type: markdown
content: "# Hello"
196 bytes, against 1.692 MB for my real config. A factor of 9000. Same URL, same dashboard, same everything — only the content changes.
Locally: load at 744 ms in both cases, and more to the point exactly the same 163 requests and the same 14 MB of decoded JavaScript. Config size changes nothing about resource loading; the frontend loads everything that’s registered regardless of what it has to display.
Through the cloud, cold — I emptied all 4 caches and unregistered the service worker before measuring:
- DOMContentLoaded: 1877 ms
- Load, meaning the HA shell alone: 11 945 ms
- last resource finished: 109 311 ms
- transferred: 1228 KB across 161 requests, so about 0.09 Mb/s
A 196-byte dashboard takes 110 seconds to show up.
One difference I hadn’t anticipated, while I’m at it: on the LAN I’m on plain HTTP, so a non-secure context, and HA’s service worker doesn’t even register there. Over the cloud, on HTTPS, it does.
So the two paths don’t have the same caching mechanism, which makes a cold comparison even less favourable to the cloud.
What happens with the real dashboard, cold
Worse: it doesn’t finish. Per the HAR, the load transfers 1.78 MB and then gives up and drops back to the login page after about a minute and a half. The WebSocket never establishes. Files of 5 to 12 KB take 15 to 23 seconds, authorize.js at 118 KB takes 16.5 seconds. The plain HA shell takes 7.4 s versus 1.2 s on the LAN.
So cold, through the cloud, my install isn’t slow, it’s unusable.
The test that isolates the culprit
I also reach my instance over OpenVPN. Same phone, same 5G, same Yellow, same dashboard: it works normally.
The difference is structural. Over OpenVPN my phone builds a direct tunnel to my router: one hop, my own latency. With Nabu Casa there is no direct path — the Yellow holds an outbound connection to the relay, my client connects to that same relay, and every byte goes up and then back down.
Same hardware, same line, same dashboard, only the route changes. That’s a controlled test, and it points at the relay.
What I ruled out along the way, in case it saves someone time
Browser Mod piling up stale browser sessions, the explanation that comes up most often: I have exactly one device and thirteen entities.
A bloated known_devices.yaml, which fixed it for someone in an older thread: mine is 543 bytes.
WebSocket saturation from too many subscriptions: 34 frames for the entire load, 170 state changes in 15 seconds. The socket is bored.
Cutting the entity count by disabling integrations. That one is a trap and I lost an evening to it.
I disabled 62 config entries covering 1756 entities, 24% of my total, and measured the payload before and after: 3.518 MB versus 3.452 MB. Under 2%. Disabled entities stay in the state machine as restored: true until HA restarts, so you measure nothing without a reboot.
And dashboard size, definitively ruled out by the 196-byte test.
Two genuine bugs I found while digging
Browser Mod downloads its 174 KB file twice on every load. It registers itself both through add_extra_js_url as /browser_mod.js?<version> and as a Lovelace resource /browser_mod.js?automatically-added&<version>. Different query string, different cache entry, two downloads. Locally that costs 16 ms plus 319 ms and nobody would ever notice. Through the tunnel it cost me 13.2 seconds plus 10.6 seconds. Deleting the Lovelace resource doesn’t stick, the integration recreates it at startup — but if you edit the resource URL so it ends with the version number, the integration leaves it alone and you get a single download.
And I had lovelace-wallpanel loading 213 KB on every page while being disabled in its own config.
Removed.
Between them, about 387 KB and 12 requests less per load. It helps, it’s nowhere near enough — of course, since the problem isn’t volume.
My questions
An aggregate cap of roughly 0.4 Mb/s on a Remote UI relay: is that expected? Fair use, QoS, or a saturated node? And if it’s the node, is there any way to move to another one? Reconnecting always lands on the same one.
Because at that rate it isn’t only my install that’s affected: the plain HA shell already takes 7.4 seconds, and an empty dashboard 110. That makes remote access unusable cold for anyone, whatever their config.
If anyone wants to compare, here’s the measurement that matters — aggregate throughput, not latency.
Run it from a terminal with a long-lived access token:
time curl -s -o /dev/null -H "Authorization: Bearer YOUR_TOKEN" \
"https://YOUR-INSTANCE.ui.nabu.casa/api/states"
Compare it with the same request against your local IP. It’s the ratio between the two that tells the story, and it doesn’t depend on any dashboard.