We Replaced the Networking Layer of Our Baby Monitor
September 20, 2026
A friend of mine test-drove an early build of EarHorn for a week. He’s a better networking engineer than I am, which is a low bar, and at the end of it he asked the question I had been avoiding.
Why is this built on Multipeer Connectivity? Wouldn’t Network.framework be better, even if getting there is a lot of work?
I gave the answer you give when you do not want to do a rewrite. It works. I’ve used it. The lift is enormous.
He offered to help with the lift.
Three words
Multipeer Connectivity is Apple’s easy answer to two phones talking with no server in between, and it is genuinely good at the hard part. It finds the other phone, handles the invitation, and manages the radios underneath, including a direct phone-to-phone link, and hides all of it so you never have to think about it.
Then it hands you one callback, which reports the other phone as being in one of three states. Not connected. Connecting. Connected.
That’s the whole vocabulary.
There is no error value in it. When a session dropped at two in the morning, I was told the peer was not connected. Not whether the router had gone away, whether the phones had been talking fine until one had its battery die, or whether the other phone was asleep in somebody’s pocket. The framework knew. It had nowhere to put that in the reply.
So I wrote the machinery to guess. Timers to tell a blip from a real disconnect. Heuristics to tell a link migration from a failure. Backoff ladders built on inference. MultipeerTransport topped out around 2,240 lines, half of it an elaborate reconstruction of facts the operating system already had and was not saying.
That was his actual point, and it was not about speed. A baby monitor spends most of its life deciding whether to trust the connection it has. The layer underneath should be willing to say what happened.
The other one makes you do the work
Network.framework is the low-level one. You bring your own listener, your own browser, your own connections, your own framing, your own idea of what a message is. Nothing is free.
What you get for the money is language. A connection says waiting, and attaches the error it is waiting on. It says failed, and attaches the error it failed with. The path tells you which kind of interface it is, whether it is a direct phone-to-phone link, whether the system thinks it is expensive. Change the route under a live connection and you hear it from the framework rather than from a stopwatch.
Whole afternoons of my inference code turned into reading a value.
Two monitors, briefly
There was an awkward middle, because the two transports could not see each other. They advertise under different Bonjour service types and speak different protocols, so a phone on the old build and a phone on the new one were each, as far as they knew, alone in the house. For a few weeks the app ran both at once behind a composite that let a nursery answer on either: another 630 lines of code whose entire reason for existing was that there were two of these.
On the 19th of August all of it came out. The old transport, the composite, and the setting that had let you pick.
The tradeoff
Swapping the networking layer did not make the connection perfectly reliable. It gave me true reasons for failures I then had to fix one at a time, on two phones in a real house, over weeks. That work is still going.
The connection will still drop. Every network path can fail, iOS suspends background apps when iOS feels like it, and no framework choice touches either fact. It is not more secure either. The same key exchange and encryption ran over the old transport and run over the new one; that layer never moved.
What the rewrite bought was honesty. The app now knows the difference between “the router blinked” and “that phone is gone,” and can act on it, which it could not do while it was guessing.
Two things worth stealing if you are doing this too. A published property in Swift fires when you assign to it, not when the value changes, so six sources feeding one piece of state will cheerfully report the same connection six times and make your own logs lie to you. And an async stream can only be iterated once, so replace one without finishing it first and the code waiting on that stream waits forever, silently, while everything above it reports itself perfectly healthy.
He was right. I’ve decided I owe him some beers.
EarHorn is a convenience tool for audio monitoring and is not a substitute for attentive supervision.