There are ZigBee bulbs at home. One evening it struck me: what if I listened to the music and drove the lights from it?
The logic looked simple. Take audio from the microphone, run a frequency analysis, find the beat, send colour and brightness on each one.
The first version worked. For five seconds.
Then what happened
A gradual collapse. First the lights fell slightly behind the music. Then the lag grew — half a second, a second. Then some commands never arrived; one bulb stuck on blue. Finally everything began flashing at random.
That is the classic signature of network congestion: first latency, then loss, then chaos.
The numbers were right there
What the bridge can actually carry: about 10 commands per bulb per second, fewer for group commands. I was sending five times its capacity.
ZigBee is a low-power, low-bandwidth network. Its purpose is collecting a few readings an hour from a sensor. Fifty commands a second is entirely outside its design intent.
And the bridge does not refuse — it accepts and queues. That is the real problem. An error would have told me instantly. Instead everything returns “success”, the queue quietly grows, and the system dies slowly.
An interface accepting your request does not mean it can do it.
The right fix: let the bulb do the work
My first reflex was to lower the send rate. But ten commands a second does not produce a smooth fade — the light jumps.
Then I noticed what the protocol was already offering: transitiontime. That field tells the bulb to reach the target colour over a given duration. The bulb generates all the intermediate values. Smoothness comes from the hardware, not the network.
// before: I was sending every intermediate step
t=0ms → brightness 40
t=30ms → brightness 80 (extra network traffic)
t=60ms → brightness 120 (extra network traffic)
t=90ms → brightness 160 (extra network traffic)
// after: one command, the bulb does the fade
t=0ms → { bri: 160, transitiontime: 1 } // reach it in 100 ms
Four commands became one. And it looked smoother, because the bulb’s own interpolation is unaffected by network jitter.
Plus queue discipline
The second layer: my own queue on the sending side, with two rules.
- A rate ceiling. At most 10 commands per bulb per second.
- Overwrite the old one. If a command for that bulb is already waiting, the new one replaces it rather than queueing behind it.
The second is critical. For a light, only the most recent state matters, not the intermediate ones. Sending a queued “go red” is pointless when it should now be blue.
Queues come in two kinds: work queues (every item must be done — orders, emails, invoices) and state queues (only the last one matters — light colour, cursor position, a temperature reading). Managing both with the same structure always accumulates lag in the second kind.
Choosing colour was harder than I expected
With the technical problem solved, an aesthetic one appeared. Mapping frequency straight to colour (bass = red, treble = blue) sounds sensible and looks ugly. The room turns into a permanently colour-shifting disco ball.
What worked was a constraint layer: a limited palette is chosen per track, and the audio only moves within that palette. White is a special case, reserved for strobes. Otherwise everything drifts to a grey-pink average.
The constraint does not reduce variety; it produces coherence. I reused this unchanged in the light show engine.
What I learned
When a system slows down the first reflex is “more, faster”. Usually the right move is the opposite: send less, and let whatever can do the work do it.
And reading the fields an interface already offers comes before writing your own solution. transitiontime was there from day one.