Skip to contentHome

When You Can't Make It Faster, Make the Waiting Intentional

Hardware latency is not an engineering problem. Or rather, it starts as one, and at some point you exhaust your engineering options and arrive at a design problem: the delay still exists, so what does the user read into it? That question is where I landed on a premium lighting product, and the answer changed the whole interaction model.

The constraint that wouldn't move

The device communicates over Bluetooth. Every command, every slider adjustment, every brightness nudge fires a BLE packet that travels to the device, gets executed, and comes back. That round trip has a floor. You can tune the connection parameters, optimize command batching, improve your retry logic. You cannot remove the physics.

The first version of the UI worked the way most hardware companion apps work: you move the slider, the command fires, a spinner appears, and the UI updates when the device confirms. This is correct behavior. It is also exhausting to use, because the core interaction on a lighting product is adjusting the light. Every adjustment producing a visible loading state makes the app feel broken. It signals that the hardware is struggling, even when it isn't.

The problem was not the latency. It was what the UI was saying about the latency.

Optimistic updates as a design decision

The engineering pattern I reached for was optimistic UI: show the result immediately, fire the command in the background, roll back only on failure. The slider moves to 80%, the interface reflects 80% instantly, the BLE command travels asynchronously. If it fails, the UI corrects itself with an error state.

This is a common enough pattern in web software. In hardware it feels slightly dangerous, because the thing on the screen and the thing in the room can now be out of sync for a fraction of a second. The bet is that the mismatch is shorter than the attention window and that the rollback, on the rare occasions it happens, is more honest than pretending commands always land instantaneously.

On a product where the core loop is adjust and observe, that bet is worth taking. The alternative is a spinner between every adjust and every observe, which breaks the loop entirely.

The screen as mirror

Optimistic updates solved the latency problem in the interaction layer. But there was a second problem underneath it.

The app and the device were aesthetically separate. You looked at a white screen to control a warm amber light. The connection between what you held and what you saw was entirely cognitive. It worked, but it didn't feel like controlling the room. It felt like typing into a terminal.

The design shift was to make the screen mirror the device. The entire background tints to match the light's current color temperature and brightness. Cool at 6500K, warm at 2700K, dim when the light is dim. The ambient state is pulled from the device and composited across the UI continuously.

Now the waiting, when it exists at all, reads as the screen catching up to the room rather than the app failing to confirm a command. The visual relationship between interface and device is direct. You glance at the screen and see the room.

When design and engineering sit in the same head

I want to be careful about what I'm claiming here. The ambient UI was not purely an engineering response to a latency problem. It was an aesthetic choice I arrived at after four design iterations, and only worked because I was building it myself and could see the interaction at runtime rather than mocking it in a design tool.

What sitting in both roles gave me was that the latency constraint and the ambient aesthetic problem were visible simultaneously. I could see that solving one could serve the other. A pure engineering pass might have landed at optimistic updates and shipped. A pure design pass might have landed at a beautiful ambient surface that still had spinners on every command.

The connection between the two was the insight. Hardware latency felt like a wall. It turned out to be a material.

The takeaway

If you're working on a product where a delay is structural and irreducible, the design question is not how to hide it. It is how to reframe it. A wait that reads as loading is a failure state. A wait that reads as feedback is part of the product.

The difference is what the rest of the interface is saying while the wait happens. If the screen is alive, if it reflects the actual state of the thing being controlled, a brief delay feels like responsiveness, not latency.