Inside the adb protocol: transports, services and port 5037
Most people meet adb as a command. You type adb devices, a list appears, and you move on. That is fine until something odd happens: a phone flips to offline for no reason, two tools on the same machine fight over a device, a Docker container cannot see phones that are plugged into the host, or wireless debugging works from one laptop and not another.
I run Android automation from Singapore, and almost every adb problem I have debugged came down to one of three misunderstandings. People confuse the adb client with the adb server. They confuse host services with services that run on the device. Or they assume the link between the server and the phone behaves like a plain TCP pipe, when it is a multiplexed, authenticated transport with its own framing. Once those three are clear, most “mysterious” failures become boring.
This article is a practitioner’s walk through the protocol layers: the smart socket on localhost:5037, the service namespace, the transport messages between server and adbd, the sync protocol behind push and pull, and the TLS-based pairing used for wireless debugging since Android 11. I will show raw socket code so you can see the framing yourself. I am working from the official adb documentation on developer.android.com and the adb source in AOSP, and where I am going from memory of the source instead of a doc page I say so. Check the source for your platform-tools version before you build anything that depends on a byte layout.
background and prior art
adb has been in the Android SDK since the first release. The architecture has barely changed: a client, a server and a daemon. The client is the adb binary you type. The server is the same binary started in the background, listening on TCP port 5037 on the loopback interface by default. The daemon, adbd, runs on each device or emulator. The AOSP adb source describes this three-part split, and the developer.android.com adb page summarises it in the section on how adb works.
The part that matters for protocol work is that there are two completely different wire protocols. Between the client and the server there is the “smart socket” protocol, a very simple text-ish request/response format over TCP. Between the server and adbd there is the binary transport protocol, with 24-byte message headers, which travels over USB bulk endpoints or over a TCP connection (and over TLS on the wireless path). The server is the translator in the middle. It owns the device connections, it owns the RSA key used for authentication, and it multiplexes many client requests onto each device transport.
Prior art is useful here, because adb borrows ideas people already know. The client-to-server side feels like a tiny version of the X11 or SSH-agent style of “connect to a local daemon and ask for a service”. The transport side looks like a stripped-down TCP: OPEN, OKAY, WRTE, CLSE messages carry stream ids, so many logical streams share one physical link. The AOSP adb directory holds protocol and services notes alongside the code. Those files have been renamed and converted between formats over the years, so browse the adb module in AOSP instead of trusting a deep link from a blog post, including this one.
the core mechanism
the smart socket on 5037
Every client invocation does the same thing. It connects to the server (starting it first if nothing answers), sends one request, reads the answer, and usually closes the socket. A request is a four-digit hexadecimal ASCII length followed by the payload. So the request host:version (12 bytes) goes on the wire as 000chost:version.
The server replies with a four-byte status, either OKAY or FAIL. After FAIL, the server sends another four-digit hex length and an error message of that length. After OKAY, what follows depends on the service. Some services return a length-prefixed blob (host:version, host:devices). Some return nothing and simply change the nature of the socket (host:transport:<serial>). Some stream until you close (host:track-devices, and every shell: style service once you are attached to a device).
Two details trip people up. The length is hex, not decimal, and it counts payload bytes only. And the “smart” in smart socket is exactly this: the same TCP connection can start as a request/response exchange with the server and then be switched into a raw byte pipe to a device service.
host services versus local services
The service namespace has two halves. Host services are answered by the server itself and never touch a device. Local services are executed by adbd on the device (or by the emulator side) and are reached by first selecting a transport.
Host services I use regularly:
host:version: returns the server protocol version as four hex digits. For the adb 1.0.41 generation of the tool, that value is0029, which is 41 decimal.host:devicesandhost:devices-l: return the device list as one length-prefixed text block, one device per line, state included. The-lvariant addsproduct:,model:,device:andtransport_id:fields.host:track-devicesandhost:track-devices-l: the same list, but the socket stays open and a fresh length-prefixed block arrives on every change.host:transport:<serial>,host:transport-usb,host:transport-local,host:transport-any: bind this socket to a specific device.host:tport:serial:<serial>: like transport selection, but the server first returns the numeric transport id (eight bytes, little endian, as I read it in the source). This is whatadb -t <id>is built on.host-serial:<serial>:<request>: a one-shot form that selects a device and runs a host-side request, such asget-state, without a separate transport step.host:connect:<host>:<port>andhost:disconnect:<host>:<port>: the TCP device registration behindadb connect.host:kill: stops the server.host:features: lists server capabilities.
Local services are the ones that feel like “adb commands” to most people:
shell:<cmd>: run a command, stream output.exec:<cmd>: the same, without a pty, so binary output is not mangled. This is whatadb exec-outuses.sync:: switches the stream into the file transfer protocol (next section).tcp:<port>,localabstract:<name>,localfilesystem:<path>,localreserved:<name>: connect to a socket on the device. These are the building blocks ofadb forwardandadb reverse.reverse:forward:...and friends: request that the device open a listener and bounce connections back to the host.tcpip:<port>,usb:,root:,unroot:,remount:,reboot:: control services onadbd.jdwp:<pid>andtrack-jdwp: Java debug wire attach points.
The important consequence is the two-step handshake for any device command. The client sends host:transport:<serial>, reads OKAY, then, on the very same socket, sends the length-prefixed local service string such as shell:ls /sdcard, reads another OKAY, and from then on the socket is a raw stream to that service. The server stops interpreting it as a request/response channel.
the server-to-device transport
Between the server and adbd, everything is a message with a 24-byte header: six little-endian 32-bit fields. They are command, arg0, arg1, data_length, data_check and magic, where magic is command XOR 0xffffffff. The command values are ASCII tags: CNXN (connect), AUTH, OPEN, OKAY, WRTE (write), CLSE (close), and SYNC. The wireless path adds STLS.
The flow when a device appears:
- Both sides send
CNXNcarrying a protocol version, a maximum payload size and a system identity string such asdevice::ro.product.name=...;ro.product.model=...;features=.... The features list in that string (for exampleshell_v2,cmd,stat_v2,ls_v2,sendrecv_v2) is how newer clients decide which behaviours they can use against older devices. - If the device requires authentication, which every device with “secure” adb does, it answers with
AUTHtype 1 (a random token). - The server signs the token with its private key (
~/.android/adbkey) and replies withAUTHtype 2. If the device does not know the key, the server sendsAUTHtype 3 with the public key, and the device shows the “Allow USB debugging?” dialog. Accepting stores the key in the device’s adb key store, and “always allow” makes that permanent for that key. - The device sends a final
CNXNand the transport becomesdevicestate.
Per-stream traffic then uses OPEN (arg0 is the host’s local stream id, payload is the service string), OKAY (stream ready, carrying the peer’s id), WRTE (data, each acknowledged with OKAY) and CLSE. That per-write acknowledgement is flow control. It is simple and it is why a stalled consumer on the host stalls only its own stream.
RSA-based authentication arrived with Android 4.2.2. Before that, anything that could reach adbd could use it. Shell protocol v2, which adds separate stdout/stderr and proper exit codes by framing the stream with a one-byte id and a four-byte length, arrived with Android 7.0. If you ever see adb shell returning exit code 0 for a failed command against an old device, that is shell v1 behaviour.
the sync protocol
push, pull, ls and stat do not go through shell:. They use the sync: service, which turns the stream into a binary request/response protocol with 8-byte headers: a four-character ASCII id and a 32-bit little-endian value.
The core requests:
STATwith a path: repliesSTATplus mode, size and mtime, three more 32-bit values. Newer devices advertisestat_v2, with a wider record, via theSTA2id.LISTwith a path: replies with a series ofDENTrecords (mode, size, mtime, name length, name) ending inDONE. TheLST2/DNT2pair is thels_v2flavour.RECVwith a path: the device streamsDATAchunks and ends withDONE.SENDwithpath,mode: the host streamsDATAchunks and ends withDONEcarrying the mtime. The device then answersOKAYorFAILplus a message.
The DATA chunk size is capped at 64 KiB by the sync layer, independent of the transport’s payload size. Around Android 11 and platform-tools 30, newer devices began advertising sendrecv_v2 plus compressed variants. The adb help in current platform-tools exposes this as adb push --sync, -z <algorithm> to pick a compression algorithm and -Z to disable compression. As of October 2026, check adb help on your own build for which algorithms it lists, since this has moved between releases.
wireless debugging and TLS
Before Android 11 the only network option was adb tcpip 5555: a USB-connected device restarts adbd listening on a TCP port, and you adb connect ip:5555. It used the same RSA AUTH exchange in the clear. Anyone on the network path could read the traffic.
Android 11 (API level 30) added wireless debugging in developer options, with adb pair. As I read the source and the docs, the model is:
- Pairing: the device shows a six-digit pairing code and advertises a pairing service over mDNS (
_adb-tls-pairing._tcp).adb pair <ip>:<pairing-port>runs a password-authenticated key exchange (SPAKE2, see RFC 9382) keyed by that code, then each side exchanges its adb public key. This one-time step is what replaces the “Allow debugging” dialog. - Connecting: the device advertises
_adb-tls-connect._tcpover mDNS on a port that is different from the pairing port. The client connects, and after the initialCNXNthe two sides exchangeSTLSand upgrade the connection to TLS. The TLS layer is a modern TLS 1.3 handshake as far as I can tell from the source (see RFC 8446 for the protocol itself), with certificates derived from the adb keys so each side can check the other is a paired peer.
After the upgrade, the same OPEN/WRTE machinery runs inside the TLS tunnel. Nothing about stream semantics changes.
worked examples
example 1: talking to the server with a raw socket
This is the best way to make the smart socket real. It needs only Python 3 and a running server (adb start-server).
import socket
def recv_exact(s, n):
buf = b""
while len(buf) < n:
chunk = s.recv(n - len(buf))
if not chunk:
raise ConnectionError("server closed early")
buf += chunk
return buf
def send_request(s, req):
s.sendall(b"%04x" % len(req) + req.encode())
def read_status(s):
st = recv_exact(s, 4)
if st == b"FAIL":
n = int(recv_exact(s, 4), 16)
raise RuntimeError(recv_exact(s, n).decode())
assert st == b"OKAY", st
def read_hex_block(s):
n = int(recv_exact(s, 4), 16)
return recv_exact(s, n).decode()
s = socket.create_connection(("127.0.0.1", 5037))
send_request(s, "host:version")
read_status(s)
print(read_hex_block(s)) # '0029' on the adb 1.0.41 generation
s.close()
s = socket.create_connection(("127.0.0.1", 5037))
send_request(s, "host:devices-l")
read_status(s)
print(read_hex_block(s))
s.close()
The request host:version is 12 characters, so the prefix is 000c. The reply is OKAY, then 0004, then the four hex digits of the version. Eight bytes of status and length plus four bytes of payload: that is the whole exchange.
example 2: a shell command through a transport
Same helpers, now with the two-step handshake.
def adb_shell(serial, cmd):
s = socket.create_connection(("127.0.0.1", 5037))
send_request(s, f"host:transport:{serial}")
read_status(s)
send_request(s, f"shell:{cmd}")
read_status(s)
out = b""
while True:
chunk = s.recv(4096)
if not chunk:
break
out += chunk
s.close()
return out.decode(errors="replace")
print(adb_shell("emulator-5554", "getprop ro.build.version.release"))
Two things to notice. After the second OKAY there is no more framing at all. You read until the server closes. And this is the legacy shell: service, so you get no exit code and stdout and stderr arrive merged. To get shell v2 framing, request the service as shell,v2,raw:<cmd> against a device whose host:features or CNXN features include shell_v2, and then decode packets of one id byte (0 stdin, 1 stdout, 2 stderr, 3 exit) followed by a little-endian 32-bit length.
The serial emulator-5554 also shows the port arithmetic behind emulators: the emulator console listens on an even port starting at 5554 and its adb endpoint on the next odd port, 5555. The server scans a small range of local ports (5555 upward in odd steps, with an upper bound that can be changed via ADB_LOCAL_TRANSPORT_MAX_PORT) to find them.
example 3: a sync push by hand
This sends a 10 MiB file in 64 KiB chunks. The arithmetic is simple: 10 MiB is 10,240 KiB, divided by 64 KiB, which is 160 DATA frames plus one SEND and one DONE.
import struct, time
def sync_push(serial, local, remote, mode=0o100666):
s = socket.create_connection(("127.0.0.1", 5037))
send_request(s, f"host:transport:{serial}")
read_status(s)
send_request(s, "sync:")
read_status(s)
spec = f"{remote},{mode}".encode() # 0o100666 -> '33206'
s.sendall(b"SEND" + struct.pack("<I", len(spec)) + spec)
with open(local, "rb") as f:
while True:
data = f.read(64 * 1024)
if not data:
break
s.sendall(b"DATA" + struct.pack("<I", len(data)) + data)
s.sendall(b"DONE" + struct.pack("<I", int(time.time())))
ident, length = struct.unpack("<4sI", recv_exact(s, 8))
if ident == b"FAIL":
raise RuntimeError(recv_exact(s, length).decode())
s.sendall(b"QUIT" + struct.pack("<I", 0))
s.close()
sync_push("emulator-5554", "big.bin", "/data/local/tmp/big.bin")
0o100666 is a regular file with rw-rw-rw- permissions, which is 33206 in decimal, so for /data/local/tmp/big.bin (24 characters) the SEND payload is /data/local/tmp/big.bin,33206, 30 bytes. I picked /data/local/tmp because the shell user can write there on non-rooted devices. Pushing to arbitrary system paths fails with a FAIL message, which is the sync protocol reporting the device’s permission error back to you. The point of the exercise is the framing, not throughput.
edge cases and failure modes
server and client version mismatch
If the client’s expected server version differs from what is running, the client prints a message like adb server version (40) doesn't match this client (41); killing... and restarts the server. That is by design. The failure mode appears when two different adb builds fight: an Android Studio SDK copy, a distro package and a copy bundled inside some other tool. Each one kills the other’s server, devices drop to offline and re-authenticate, and long-running sessions die. The counter-strategy is boring: one platform-tools install per machine, one PATH entry, and tools told explicitly which adb to use. When several tools share one host, I pin them to the same server with -P or ANDROID_ADB_SERVER_PORT, or give each its own port on purpose.
exposing port 5037
By default the server binds loopback only. adb -a (or -a nodaemon server) listens on all interfaces, and the smart socket has no authentication. Anyone who can reach that port can run host:transport then shell: on every authorised device. That includes the keys you already approved on the phones. Containers make this tempting, because a container cannot see the host’s loopback. I covered the container side in running UiAutomator2 device automation inside a Docker container. The pattern I trust is to keep the server on loopback on the host and bridge it through an SSH or WireGuard tunnel, or to run the server inside the container with USB passed through. If you must use -a, firewall it to the one client address that needs it.
offline, unauthorized and the other states
host:devices reports a state string, and each state means something specific: device (usable), offline (transport exists, handshake not complete or link broken), unauthorized (the device has not accepted this server’s key), authorizing, no permissions (a host udev problem on Linux), bootloader, recovery, sideload. The frequent real cause of repeated unauthorized is a lost or regenerated ~/.android/adbkey, for example a CI image or container that creates a new home directory every run, so the device sees a brand-new key every time. Persist the key, or point ADB_VENDOR_KEYS at a stable one. This is also one of the quiet differences between a hand-held desk phone and a device farm: in a farm the key has to be a managed secret, not an accident of whoever’s home directory ran the job.
wireless debugging surprises
On Android 11 and later, wireless debugging has several separate moving parts, and each fails differently. mDNS discovery is multicast, so it often does not cross VLANs or guest Wi-Fi isolation, while a direct adb connect <ip>:<port> still works. The connect port is not the pairing port, and it changes whenever wireless debugging is toggled, so scripts that hard-code it break. The pairing code is short-lived. Classic adb tcpip 5555 still works after Android 11, but needs a USB connection first and does not survive a reboot. When discovery looks flaky, adb mdns check and adb mdns services show what the host actually sees, and they are cheaper than guessing.
shell versus exec for binary output
adb shell screencap -p > shot.png was a classic trap on older devices because the pty layer translated line endings in the stream and corrupted the PNG. adb exec-out screencap -p > shot.png avoids the pty entirely and is the documented way to move binary data. On Android 7.0 and newer, shell v2 fixes most of this when the client and device both support it, but I still use exec-out for anything binary because it makes my intent obvious in the script.
remote devices and the transport you can reach
Everything above assumes the server can reach adbd: USB, a LAN address, or a tunnel. People hit this wall when the phone is somewhere else. For what it is worth, cloudf.one rents real Android phones in Singapore on dedicated hardware, controlled from the browser. 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]), and it is Singapore-only. I mention it only because it is the same shape of problem: a reachable transport is a networking question first and a protocol question second.
what we learned in production
The biggest lesson is to treat the adb server as a piece of shared infrastructure with one owner. Most of the flakiness I see in other people’s setups is two things both thinking they own port 5037: an IDE and a test runner, a desktop GUI wrapper and a script, or two Appium sessions on different SDK installs. When I add tooling, I decide who starts the server, who is allowed to run kill-server, and which build of platform-tools is canonical. Scrcpy-based workflows are a good test of this. scrcpy uses adb to push its device-side server and to set up a forward or reverse tunnel, so a mismatched adb shows up there first. I wrote up two practical uses in using scrcpy to turn an Android phone into a webcam and using an Android phone as a PC microphone through scrcpy. Both lean on that tunnel behaviour.
The second lesson is that watching state beats polling it. host:track-devices gives you transitions as they happen, with one long-lived socket, so a farm controller can react to a phone going offline instead of discovering it on the next failed command. The third is to be honest about permissions: what a shell-user adb session can do depends on whether you have root, and that changes which of the local services above even work. I compared those cases in root vs non-root Android automation. Only automate apps and devices you own or are authorised to test. adb gives you a lot of reach, and the protocol does not check your intentions. For a wider list of related write-ups, the blog index is the place to start.
references and further reading
- Android Debug Bridge (adb), Android Developers: official command reference, wireless debugging steps and environment variables.
- AOSP platform/packages/modules/adb: the adb source and its protocol, services and sync notes.
- RFC 8446, The Transport Layer Security (TLS) Protocol Version 1.3: the TLS specification behind the wireless transport upgrade.
- RFC 9382, SPAKE2, a Password-Authenticated Key Exchange: the class of exchange used when pairing with a short code.
- scrcpy on GitHub: a real-world client that builds on adb’s push, forward and reverse services.
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-22.