Power, Duty Cycle, and TCP: Managing Mobile Device Radios for Reliable Connections
I once watched a classroom demo go sideways in the most instructive way possible. A phone, mid-stream, went dark on the packet capture for three hundred milliseconds, woke up, and the whole video stuttered like a scratched record. Nobody touched the network. The radio just went to sleep. That's the whole story of this piece: your socket isn't broken; your radio has a bedtime, and TCP takes the blame every single time.
Here's what we're doing. We're tracing the wire from the antenna all the way to the congestion window, and I'll hand you fixes you can test before lunch.
The three words that actually matter
Power is the strength of the signal a radio pushes out, measured in milliwatts or dBm if you like your units logarithmic and slightly sadistic. Duty cycle is the ratio of time-on to time-off, the rhythm a radio breathes in. Radio state is just where the hardware sits on any given millisecond: wide awake and transmitting, dozing, or somewhere in that groggy wake-up stretch. Save your breath arguing over which one matters most for TCP, because the answer is all three, tangled together. Duty cycling keeps batteries alive for a workday instead of a lunch break, but it smuggles in latency and jitter that TCP reads as packet loss. That single sentence explains half the "my app is laggy" tickets I've seen in twenty years.
The ways your device conspires to nap
Wi-Fi power save mode, the venerable 802.11 PSM, has clients sleeping between beacon intervals and waking on a schedule tied to the DTIM count. Stretch that DTIM out and you save battery at the cost of a client that's unreachable for longer stretches. WMM-PS flips the script slightly: the access point buffers frames and waits for a trigger before delivery, which sounds tidy until your interactive socket needs an answer right now and gets queued behind a nap.
Cellular is its own beast entirely. LTE and 5G run DRX cycles, discontinuous reception windows where the modem checks in only periodically for paging, and the low-power IoT profiles push these sleep windows into territory measured in seconds, sometimes minutes. Add firmware-level duty-cycle enforcement, antenna switching logic, and regulatory transmit ceilings, and you've got a stack of independent sleep decisions none of which asked your application for permission.
And here's the rub: Android, iOS, and every chipset vendor implement this differently. What works beautifully on one Samsung model turns into a battery-saving nightmare on a budget Android handset three years older. So, test broadly or don't trust your assumptions.
Where TCP starts apologizing on your behalf
A sleeping radio injects latency that's part predictable, part wildly variable, and TCP's RTT estimator hates variable almost as much as it hates loss. The retransmission timeout calculation gets poisoned by samples taken during a wake-up spike, and suddenly your connection thinks the network is congested when really it's just yawning.
Spurious retransmissions follow naturally. A packet sent into a radio that's mid-sleep looks, from TCP's perspective, exactly like a packet that vanished into the ether. Duplicate ACKs pile up, congestion control kicks into gear, and your throughput tanks even though every byte eventually arrived intact.
Slow start suffers worst of all. A flow that can't sustain its congestion window because the radio keeps interrupting delivery never climbs out of the kiddie pool. Add delayed ACKs, Nagle's algorithm bunching up small writes, and keepalive timers firing at exactly the wrong cadence, and you've got amplification on top of amplification. Worse still, NAT tables and middleboxes have their own patience limits. A radio asleep too long risks its ephemeral port mapping expiring entirely, and the next wake-up greets a connection reset instead of a clean handshake.
Call it what it is: most "flaky network" complaints are radio policy dressed up as a networking bug.
The constraints nobody asked your socket about
Battery management, thermal limits, background task scheduling, and OS-level batching such as Android Doze or iOS Background App Refresh all exist specifically to force radios into restrictive duty cycles. App-level timers pile on top. None of this is accidental. It's deliberate energy triage, and your TCP stream is collateral.
What you can actually do about it
Set TCP_NODELAY on control traffic where latency trumps efficiency, but understand you're trading small-packet overhead for responsiveness. Tune your keepalive interval and probe count to stay just ahead of NAT timeout windows rather than hammering the radio awake every thirty seconds out of habit. Consider TCP_USER_TIMEOUT or a sensible SO_RCVTIMEO so a socket fails fast instead of hanging in limbo when you already know the radio policy is hostile.
On the application side, batch your small messages into deliberate bursts rather than a steady drip of heartbeats. Make your heartbeat adaptive: lengthen the interval when idle, shorten it right after activity, and sprinkle in jitter so you're not synchronizing wake-ups across a thousand devices at once. Push notifications and server-side wake proxies can shoulder the keepalive burden entirely, letting the device rest while the server handles presence. When retries are necessary, use exponential backoff with randomization, because a rigid retry schedule just collides with the next sleep cycle.
One honest caveat: every "always-on" fix costs milliamp-hours. Measure the energy bill before you ship it.
Tuning the network side of the handshake
DTIM intervals and WMM-PS settings on your access point are a dial, not a switch. Shorter intervals mean snappier wake-ups and thirstier batteries. AP-side buffering, keepalive proxying, even TCP splicing can shield mobile clients from NAT expiry without forcing the device to stay chatty. Servers can extend ephemeral port and NAT mapping lifetimes for recognized mobile clients, though that's a security tradeoff worth documenting rather than quietly ignoring.
Finding the proof in your own traces
Reach for tcpdump or Wireshark to inspect RTT distribution and duplicate ACKs at the packet level, tshark if you want it automated. Pull wlan logs for sleep and wake events, modem logs for DRX cycle timing, and a power profiler to correlate energy draw with network behavior. Track retransmissions, RTO triggers, and how connection lifetime compares against your NAT's timeout window. Run the experiment properly: vary your DTIM or heartbeat interval deliberately, keep background traffic steady, and capture radio and socket traces side by side so you're comparing apples to apples.
A small, honest before-and-after
A chat app shipped with 15-second heartbeats, waking the radio constantly, burning 30% more battery than it should, and scattering spurious retransmits across every trace. Swapping to an adaptive 60-second idle heartbeat plus a server-side push proxy settled the RTTs, cut the retransmits dramatically, and kept user-perceived latency well within tolerance.
Measure before you touch anything. Tune keepalives and batching with intent. Lean on server proxies where you can. Write down what the battery costs you.
Don't blame TCP alone. Manage the radio duty cycle, or your sockets will keep apologizing for a fight they never started.