← all articles

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 is 0029, which is 41 decimal.
  • host:devices and host:devices-l: return the device list as one length-prefixed text block, one device per line, state included. The -l variant adds product:, model:, device: and transport_id: fields.
  • host:track-devices and host: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 what adb -t <id> is built on.
  • host-serial:<serial>:<request>: a one-shot form that selects a device and runs a host-side request, such as get-state, without a separate transport step.
  • host:connect:<host>:<port> and host:disconnect:<host>:<port>: the TCP device registration behind adb 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 what adb exec-out uses.
  • 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 of adb forward and adb 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 on adbd.
  • jdwp:<pid> and track-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:

  1. Both sides send CNXN carrying a protocol version, a maximum payload size and a system identity string such as device::ro.product.name=...;ro.product.model=...;features=.... The features list in that string (for example shell_v2, cmd, stat_v2, ls_v2, sendrecv_v2) is how newer clients decide which behaviours they can use against older devices.
  2. If the device requires authentication, which every device with “secure” adb does, it answers with AUTH type 1 (a random token).
  3. The server signs the token with its private key (~/.android/adbkey) and replies with AUTH type 2. If the device does not know the key, the server sends AUTH type 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.
  4. The device sends a final CNXN and the transport becomes device state.

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:

  • STAT with a path: replies STAT plus mode, size and mtime, three more 32-bit values. Newer devices advertise stat_v2, with a wider record, via the STA2 id.
  • LIST with a path: replies with a series of DENT records (mode, size, mtime, name length, name) ending in DONE. The LST2/DNT2 pair is the ls_v2 flavour.
  • RECV with a path: the device streams DATA chunks and ends with DONE.
  • SEND with path,mode: the host streams DATA chunks and ends with DONE carrying the mtime. The device then answers OKAY or FAIL plus 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._tcp over mDNS on a port that is different from the pairing port. The client connects, and after the initial CNXN the two sides exchange STLS and 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

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.

free download
The adb and scrcpy command cheat sheet

The commands we reach for on a rack of real phones, on one printable page. Leave your email and we will also tell you when we publish a new guide, a few times a month at most.

from the team behind cloudf.one
Passes on the emulator, fails on a real phone?

cloudf.one rents real Android phones in Singapore that you control from the browser, each on a persistent Singapore mobile IP. Real hardware, a real OEM build and a real carrier network, without buying and racking the phones yourself.

test on a real phone →
read on
More from ADB Handbook

adb commands, scrcpy flags, automation frameworks, and why a test passes on one phone and fails on the next.

browse all articles →