May 24, 2026
Sub-second ADB: how we connect you to a phone over Tailscale
Most remote-device platforms make you wait fifteen seconds for a session to start. We get a working ADB shell to a physical phone in under a second. Here is the connect path.
When a customer clicks "Rent device" on DeviceRent, a live ADB shell appears in under a second. That number is on the homepage, and we get the same question every time someone reads it: how?
The honest answer is "we removed the cold start that most platforms build in." This is a walkthrough of what the connect path actually does, in the order it does it.
Correction, August 2026. An earlier version of this post described customers running their own Tailscale node and connecting their local
adbstraight into our tailnet. That was never how the product worked — it described a design we prototyped and did not ship. The corrected path is below. Our apologies to anyone who went looking for atailscale upstep that does not exist.
What we do not do
Before the steps, the omissions. We do not:
- spin up a VM,
- allocate a container,
- provision a TURN relay,
- negotiate a WebRTC offer,
- or wait for a userland ADB daemon to come up cold.
Every one of those is a multi-second cost that most cloud-device platforms eat because they treat each session as a fresh sandbox. We treat the device as a long-running resource that is always reachable, and we treat the session as a thin permission layer over the top.
Step 1 — the device is already on the network
Each phone in the rack is attached over USB to a small controller — a Raspberry Pi 5 — which runs a Tailscale node. That node is up at all times. The phone's ADB port is never exposed to the public internet; it is reachable only from the controller sitting two feet away from it, over the cable.
When we say "the device is online," we mean the controller has a stable WireGuard tunnel to the coordination plane and the phone is answering a heartbeat over USB.
That heartbeat is the only thing on the hot path that runs continuously. It costs nothing in latency at session start because the tunnel is already built — no handshake, no key exchange, no DNS round-trip.
Step 2 — the customer clicks Rent
The web app calls our backend, which does several things in one pass:
- Confirms the customer owns an active rental and is eligible — a paid tier with a positive balance, or a dedicated slot, which is balance-exempt.
- Runs the health guards: a phone that is over-temperature or mid-sanitisation cannot be handed out.
- Reserves the device row in Postgres.
- Mints a short-lived HS256 JWT scoped to exactly one device serial — a ten-minute TTL, re-minted about two minutes before it expires.
This is tens of milliseconds. Postgres is fast when the locked row count is one and the index is hot.
The serial scoping is the part that matters. The token is not a key to the rack; it is a key to one phone. It is also never the shared bridge credential — that value never leaves the server.
Step 3 — the browser connects to the device
Here is the part the original version of this post got wrong.
You do not run Tailscale. You do not install anything. The tailnet is ours, and you never join it.
The controller publishes a single authenticated endpoint over a Tailscale Funnel — a public HTTPS address served from the Pi. Your browser opens a WebSocket to it and presents the JWT from Step 2 in the Sec-WebSocket-Protocol header, never in the URL, so it cannot leak into a proxy log or a browser history entry.
your browser
→ HTTPS/WSS (Tailscale Funnel)
→ bridge on the controller ← verifies JWT, pins to one serial
→ adb server
→ USB → your rented phone
The bridge on the controller is not incidental — it is the enforcement point. It verifies the token and pins the connection to the one serial that token names. Every ADB command is checked against that pin. This is what makes a shared rack safe: there is no request you can construct from a session on phone A that reaches phone B.
Once the socket is open you get a real ADB shell and a screen mirror driven by scrcpy, in the browser.
Step 4 — or point your own tooling at it
If you would rather drive the phone from Appium, Gradle, Espresso or CI, there is a connector:
npx devicerent-connect --key drauto_... --rental <rental-id>
It runs a small local adb-server emulator on your machine and tunnels each adb conversation over that same serial-pinned bridge, authenticating with an automation API key instead of a browser session. Your tools point at it with a normal environment variable:
export ANDROID_ADB_SERVER_PORT=15037
adb devices # your rented phone, as a local device
The connector implements adb forward itself — binding the port on your machine and tunneling each connection through — which is what lets Appium reach the UiAutomator2 server on the device.
Why this is fast
There is no cold start anywhere in the path. The controller's WireGuard tunnel was already up before you clicked. The phone was already awake and answering. The adb server was already running. What happens when you click Rent is an eligibility check, a row lock, and a signature — and then you are talking to a socket that was already listening.
Compare that to the standard model, where a session means allocating a container, attaching a device to it, booting a userland daemon, and negotiating a media channel. Those platforms are not badly built; they are paying for isolation guarantees they have chosen to make. We made a different trade: the device is a persistent resource, and the session is permission to reach it.
The remaining latency is mostly your distance to the rack and your own connection. We cannot do anything about either.
What we actually gave up
Three real things, and it is worth naming them.
We depend on Tailscale. If their coordination plane goes down, new tunnels cannot be established — existing ones keep working. We accept that because the alternative is running our own NAT-traversal stack: either a relay that adds tens of milliseconds, or a custom WireGuard-over-WebSocket fallback that takes a year to get right and would be worse.
Bandwidth is not USB. Traffic crosses a Funnel. A large APK push is slower than a phone on your own cable. You are trading throughput for a device you do not have to own.
The connector does not do everything. adb reverse is not supported — it forwards ports to the device, not back from it, so frameworks that need the phone to dial into your host, like Detox, will not run. Maestro is not supported either; it does not drive devices through the adb server the connector provides, and we do not list things as working until we have proven them end to end.
The point
Most "cloud" products have to be slow because their architecture forces a cold start. We do not have a cold start. The device is hot, the tunnel is hot, and the authorisation is one signature scoped to one serial.
The session is just permission to talk to a port that was already listening. That is the whole trick.