Host limits when running many Android phones: USB, power and adb
Everyone who runs more than a handful of real Android phones from one computer hits the same wall, and it rarely shows up where they expect. the first four phones work fine. at eight, one of them starts dropping out of adb devices every few hours. at twelve, the hub gets warm and two phones show “unauthorized” after a reboot. at twenty, the host is at 90% CPU and nobody can say why, because nothing is actually running except mirrors. none of this is a bug in one component. it is four separate budgets (USB topology, power, the adb server, and decode or CPU cost) running out at different times, and each one fails in a way that looks like a flaky phone.
I run real phones in Singapore for testing and automation work, and most of what follows came from being wrong about which budget was the problem. the stakes are practical. a flapping device in the middle of a long job costs you the job. a hub that browns out can corrupt a phone mid-write. a fleet that only works while you watch it is not a fleet. so I want to be mechanical about this: what each limit is, how to compute it before you buy hubs, and how to see it failing before it takes a job down.
this is not a “how many phones can I connect” article with a magic number, because there isn’t one. the answer depends on your controller, your hub chain, your charger budget, your adb usage pattern and whether you mirror screens at all. I will not invent device counts. I will show you how to work out your own, and where the published specs stop and your hardware starts. I assume you already know adb devices, what scrcpy is and why you would want a rack of real phones. if you do not, start with the best open source Android device farm tools and come back.
background and prior art
the USB side of this is older than Android. the USB 2.0 specification, published through the USB Implementers Forum document library, gives a host controller a 7-bit device address space, which means at most 127 devices per bus including hubs. it also limits hub chains to five hubs deep, which is seven tiers counting the root hub and the device. a high-speed bus signals at 480 Mbit/s and every device behind one root port shares that. a phone talking adb over USB is almost always a USB 2.0 high-speed device even when it is plugged into a blue USB 3 port, because the adb interface on most phones negotiates USB 2.0 data lines. so a USB 3 port or hub does not give you 5 Gbit/s for adb. it gives you a separate, parallel USB 2.0 path on the same connector.
the second piece of prior art is the xHCI specification Intel published, which most modern controllers implement. the spec allows a controller to expose a number of device slots and each device consumes slots and endpoint contexts. what is implemented in real silicon is smaller and varies by vendor, and that is why two machines with the same number of ports can support very different numbers of phones. the spec number is a ceiling that no one reaches. the practical limit is how many endpoints the controller and its driver can schedule, and you find that out by plugging in devices until enumeration starts failing.
on the Android side, adb itself is documented in Android’s official adb documentation and its internals in the adb OVERVIEW document in the AOSP source. the important design fact is that adb is a client, server and daemon system. one server process on the host (default TCP port 5037) multiplexes every client command to every device’s adbd. there is one server per host user by default, and all of your scripts, your IDE, your scrcpy windows and your logcat tails share it. so there is a second kind of limit hiding behind the USB one. scrcpy, from the Genymobile scrcpy repository, sits on top of that server and adds a heavy per-device cost, which is video decode.
the core mechanism
it helps to treat each phone as consuming four things from the host, and to count each one separately.
1. USB topology: ports, controllers and tiers
a physical port is not a controller. most desktop boards expose a dozen or more ports that hang off one or two xHCI controllers, and the rear ports, front-panel header ports and any hub ports all compete for the same root ports and slots. a hub does not add controller capacity. it fans out one upstream port, so ten phones behind one hub are all sharing one upstream link, one set of slots, and one transaction scheduler.
on Linux, lsusb -t prints the tree with the speed of each link and which controller (the bus number) each branch belongs to:
lsusb -t
read it for three things: how many devices sit on each bus, whether any phone shows 480M under a 5000M hub (normal, as explained above), and how deep the chain goes. on Windows, Microsoft’s USBView and the free USBTreeView do the same job. if everything hangs off bus 1, you have one controller, and adding hubs only adds more branches on the same trunk.
the way to get more capacity is more controllers, not more hubs. a PCIe card that carries several independent xHCI controllers gives each group of ports its own slot budget and its own bandwidth. some multi-port cards are really one controller plus an internal hub, and the card’s block diagram or lsusb -t output tells you which. I would rather buy a card and read its tree than trust the box description.
bandwidth is the least likely of the USB limits to bite for adb-only work. adb shell commands, adb install and logcat are small compared with 480 Mbit/s. it matters when you stream scrcpy video from many phones through one bus or push large APKs and OBB files to many phones at once. scrcpy’s default video bit rate is 8 Mbit/s per device (check the scrcpy video docs for your version), so the arithmetic is simple: n phones times your chosen bit rate, compared against the realistic throughput of one bus, which is well below the theoretical 480 Mbit/s.
2. enumeration and error handling
when a phone is plugged in, the host assigns an address, reads descriptors, selects a configuration and loads a driver. a controller that is out of slots or endpoint resources fails here, and the failure looks like a dead phone or a bad cable. on Linux the kernel log is the first place to look:
sudo dmesg -w
messages such as “device not accepting address” and “error -71” point at signal quality, cables or hubs. messages about not enough bandwidth or resources point at controller limits. I have lost days on cables that I replaced one at a time before reading dmesg, which would have told me it was a power or enumeration issue immediately.
a second Linux-specific limit sits in usbfs. libusb-based tools, and adb when it uses its libusb backend, allocate from a pool controlled by usbcore.usbfs_memory_mb. you can read it at /sys/module/usbcore/parameters/usbfs_memory_mb. if many devices transfer at once and you see ENOMEM-like errors, raising it is a known fix, with 0 meaning unlimited. adb’s own help output lists ADB_LIBUSB as the environment variable that chooses the backend, so check adb --help on your platform-tools build for the exact wording rather than trusting a blog.
also on Linux, USB autosuspend can put idle devices to sleep. the kernel documentation on USB power management describes the per-device power/control file, where on disables autosuspend. a phone that vanishes after idle and returns on reconnect is a classic symptom. on Windows the equivalent is the “USB selective suspend” power setting.
3. power: what a port gives and what a phone takes
a standard USB 2.0 downstream port is specified to deliver 100 mA at 5 V by default and up to 500 mA after the device is configured. the USB battery charging specification (BC 1.2, published through the same USB-IF library) defines dedicated charging ports and charging downstream ports that can supply up to 1.5 A. most hubs and most motherboard ports only guarantee the 500 mA standard figure, unless the hub explicitly advertises charging support.
now put a phone on it. a phone with its screen on, running scrcpy’s encoder, with Wi-Fi or mobile data active, can draw more than a 500 mA port supplies. the battery then discharges while “charging”, or charges very slowly, and the phone reads as plugged in while its percentage falls. that is not an adb problem and no adb setting fixes it. you can see it directly:
adb -s SERIAL shell dumpsys battery
the output includes level, status, plugged state and temperature in tenths of a degree Celsius (a reading of 312 is 31.2 C). I log level and temperature per device on a timer. a falling level while plugged in is the earliest warning I get of an underpowered port.
the budget calculation is plain arithmetic. total supply watts must exceed the sum of per-phone worst case at 5 V, plus headroom, and it must do so on a powered hub or dedicated charger, not through the host. a bus-powered hub gets its power from the upstream port and cannot feed ten phones. a self-powered hub has its own adapter, and the adapter’s rating is the real limit. cable resistance then eats voltage. thin or long cables drop several tenths of a volt at 1 A, and a phone that sees 4.6 V at the connector may refuse to charge at full rate or reset when the screen turns on.
there is also a decision about data and power separation. some operators run data through a hub and power through separate chargers, using cables or adapters that carry data only. that removes the power budget from the hub entirely and is how I would build anything beyond a small bench.
4. the adb server and per-command cost
adb’s design is one server, many transports. each USB device gets a transport and the server multiplexes streams over it. the load on the server grows with the number of devices and the rate of commands, but it is usually not what saturates first. what saturates first is the cost on the phone, because every adb shell invocation starts a process on adbd, and a poll loop that runs dumpsys or uiautomator dump against thirty phones every second is thirty process launches on thirty small CPUs.
three adb behaviours matter at scale:
- device selection: with more than one device you must pass
-s SERIALor-t TRANSPORT_IDon every command.adb devices -lprints both the serial and ausb:path, plustransport_id. serials are not always unique on cheap phones, so I key on transport id or USB path when serials collide. - server lifecycle:
adb kill-serverandadb start-serverrestart everything. if one script restarts the server, every other script loses its connections for a few seconds.adb -P PORTor theANDROID_ADB_SERVER_PORTenvironment variable lets you run a separate server per group of phones, so a restart in one group does not touch another.adb --helpdocuments these. - authorization: the host’s RSA public key is stored on each phone when you accept the prompt. on Android 11 and later, authorization expires after a period unless you disable the timeout in developer options, so a fleet that sat untouched can come back all “unauthorized”. if the USB debugging toggle is greyed out when you go to fix it, that is a separate problem, covered in fixing a greyed-out USB debugging toggle on Android.
a simple fan-out pattern, run in parallel rather than serially:
for s in $(adb devices | awk 'NR>1 && $2=="device" {print $1}'); do
adb -s "$s" shell getprop ro.build.version.release &
done
wait
the & and wait matter. a serial loop over thirty phones turns a half-second command into fifteen seconds. but unbounded parallelism over a large fleet hits the server and the phones all at once, so I cap it, for example with xargs -P 8.
5. scrcpy: decode, render and encode cost
scrcpy is the part of the stack that scales worst. for every mirrored phone, the phone encodes video with its hardware MediaCodec encoder (heat and battery), sends it over adb, and the host decodes it with FFmpeg and renders it through SDL. decode is CPU work, rendering is GPU or CPU work depending on the renderer, and each window is its own process with its own threads. whether you need a dedicated GPU is a fair question, and I covered it in do you need a dedicated GPU to run scrcpy. short version: for a few windows no, for many large windows the render path starts to matter.
the controls that reduce per-device cost are the ones in scrcpy’s own docs, and they are the first thing I tune:
scrcpy -s SERIAL --max-size 720 --max-fps 15 --video-bit-rate 2M --no-audio --window-title "rack-03"
--max-size reduces resolution, --max-fps caps frame rate, --video-bit-rate sets bit rate and --no-audio skips audio capture (audio forwarding needs Android 11 or later and scrcpy 2.0 or later anyway). check each flag against scrcpy --help for the version you installed, since options have been added across 2.x and 3.x. if you only need to record rather than watch, scrcpy can record with --record file.mp4 and, depending on version, run without playback so it skips display work. I would verify on your build by watching CPU, not by assuming. for lower-latency tuning the trade-offs run the other way, see the best scrcpy configuration for low-latency mobile gaming.
one more scrcpy detail. when it falls back to adb forward instead of adb reverse, it uses a local port range (27183 to 27199 by default, adjustable with --port). that is a small range, so check the docs for your version if you ever run more concurrent sessions than that and see port binding errors.
worked examples
these are planning calculations, not benchmarks. I am showing the method with round numbers. replace them with your own measurements.
example 1: power budget for an 8-phone shelf
say eight phones on a shelf, all charging while running adb jobs, none mirrored. assume worst-case draw of 1.5 A per phone, which is the BC 1.2 ceiling for a charging port and a conservative number for a phone with the screen on.
- worst case total: 8 x 1.5 A = 12 A at 5 V = 60 W
- headroom: I want at least 25% over the worst case, so a supply rated for 75 W or more
- result: one charger-style 60 W hub is too tight. either a 75 W+ supply feeding a powered hub that actually passes that through, or two separate 40 W units, four phones each
then I check the real number. I plug the phones in, run a representative job, and sample dumpsys battery on each device for an hour. if levels climb or hold, the budget is fine. if any phone’s level falls while plugged in, that port is under-delivering and I move that phone to a different supply. I deliberately do not quote hub prices here because they move, and the right hub depends on whether it forwards charging current at all.
example 2: mirror cost for 12 phones on one host
now twelve phones, all mirrored at once for a monitoring wall. USB bandwidth is the easy part: 12 x 2 Mbit/s at the settings above is 24 Mbit/s, far under what one high-speed bus carries. the real question is CPU.
- measure: start one scrcpy window with your chosen settings and read the host CPU use from Task Manager or
toponce it is stable - multiply: that number times 12, as a first estimate
- headroom: my own rule of thumb is to keep a third of the CPU free for adb, the OS and whatever automation runs alongside, so the multiplied figure should stay under about two thirds of total capacity
if the estimate is over, there are three levers in order of effect. lower --max-size, lower --max-fps, and mirror only the phones someone is looking at, leaving the rest as adb-only. a wall where everything is mirrored all the time is almost never needed. a wall where phones attach to a mirror on demand is cheap. the best adb GUI tools article covers front ends that do on-demand attach if you do not want to script it yourself.
example 3: adb poll load for 30 phones
thirty phones, no mirroring, and a monitoring script that wants each phone’s foreground app and battery every few seconds.
- naive: 30 phones x 2 commands x one poll every 2 seconds = 30 shell invocations per second, each launching a process on a phone
- better: combine into one
adb shellcall per phone that runs both commands and returns one blob, so 15 invocations per second, and poll every 10 seconds instead, which brings it to 6 per second - better still: run the poller from several workers with
xargs -P, and split the fleet across two adb servers on different ports if one server restart is hurting you
the lesson is that the host rarely runs out of adb throughput. the phones run out of patience. a cheap phone doing a dumpsys every second will run warm and slow, and you will blame the host.
edge cases and failure modes
- phones dropping off after hours: usually autosuspend, selective suspend or a marginal hub. check
dmesgfor resets, disable autosuspend on that port, and swap the cable before the hub. a cable that charges fine can still be bad for data. counter-strategy: log every disconnect with a timestamp and theusb:path, and see whether the same physical port keeps showing up. - everything “unauthorized” after a host change: adb keys are per user on the host, normally in
~/.android/adbkey. a new machine, a new user or a wiped home directory means a new key, and every phone prompts again. on Android 11 and later the authorization timeout can also revoke old keys. counter-strategy: copy the key pair to the new host if you own both, tick “always allow”, and turn off the authorization timeout in developer options where your Android version offers it. - duplicate or missing serials: some inexpensive phones report the same or an empty serial, and
-sbecomes ambiguous. counter-strategy: useadb -t TRANSPORT_IDfor the life of a session, and map USB path to a rack position in a small table so you know which physical phone is which. the transport id changes on reconnect, the USB path does not unless you move the cable. - installs failing on one brand only: a fleet-wide
adb installloop will hit vendor restrictions on some phones. on Xiaomi and Redmi it is often a user-restriction setting, covered in fixing INSTALL_FAILED_USER_RESTRICTED when adb installing on Xiaomi and Redmi phones. counter-strategy: run installs per device, collect exit codes, and retry only the failures instead of letting one brand stall the loop. - thermal throttling and battery wear: phones that are mirrored, charging and running jobs get hot, and a hot phone throttles, which makes your automation timing drift. phones that sit at 100% charge for months age their batteries and can swell. counter-strategy: read temperature from
dumpsys battery, space the phones out for airflow, and use any charge-limit feature the phone has. I would also rather replace a swollen battery than keep running it.
wireless adb is the usual escape hatch when USB limits bite. adb pair and adb connect work from Android 11 onward through wireless debugging. it removes the controller and hub limits but trades them for Wi-Fi congestion, mDNS quirks and a pairing step per device, so I treat it as a complement and not a replacement. phones still need power either way.
what we learned in production
the thing I would tell my earlier self is to stop adding hubs and start adding controllers and separate power. every time I solved a flapping phone by buying a “better hub” I had actually fixed a power problem that happened to live in the hub. once I separated data from power, a lot of the mysterious disconnects stopped being mysterious. the second lesson is to measure per phone before multiplying. one phone’s CPU, one phone’s draw, one phone’s command rate. everything else is multiplication, and multiplication is much cheaper to get wrong on paper than on a rack.
the other lesson is operational. a fleet needs a log more than it needs a dashboard. a line per disconnect with a timestamp, the USB path and the last battery reading answers most “why did it die” questions in under a minute. I keep that, plus a map from rack position to USB path, and I check the map whenever someone touches a cable. if you would rather not own the USB, power and thermal side at all, cloudf.one rents real Android phones in Singapore on dedicated hardware, each with a persistent Singapore mobile IP, controlled from the browser. it is Singapore-only, and it has no self-service adb. adb and Appium reach a rented phone only through a WireGuard tunnel that we set up on request (email [email protected]). so it suits browser-driven work on phones you do not want to host, and it is not a drop-in for an adb-heavy rack. as always, only automate apps and devices you own or are authorised to test.
references and further reading
- Android Debug Bridge (adb) documentation, Android Developers
- adb OVERVIEW, Android Open Source Project
- scrcpy, Genymobile on GitHub
- USB Implementers Forum document library
- USB power management, Linux kernel documentation
more on this site: the blog index lists every deep-dive, and the best open source Android device farm tools is the right next read if you are deciding between building a rack and using existing farm software.
Written by Xavier Fok
disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-10-04.