Binder threads and performance
Binder is cheap per call, but it is not free, and the pool behind every service is finite. The interesting failures are the ones where the transport is working perfectly and the system is still stuck.
A Binder call takes microseconds. A Binder system can still take seconds, because the call is queued behind a thread pool that a slow method has exhausted, or because a client is holding a lock while waiting for a reply that will never come.
TL;DR#
- Each process that serves Binder calls has a thread pool. libbinder's default is 15 threads, spawned on demand and kept alive afterwards.1
- Every call occupies one pool thread for its whole duration. A method that blocks for a second blocks one-fifteenth of the server's capacity for a second.
- Never hold a lock across a Binder call. The call can re-enter your process, and the re-entrant thread will wait on the lock you are holding.
onewayis not a performance fix by itself: it removes the reply wait but still consumes a pool thread on the server and still obeys flow control.- Priority inheritance lets a server thread run at the caller's priority, which is what keeps a UI-thread call from being starved by background work.
Mental model#
Think of a bank with a fixed number of tellers.
Customers arrive and take a number. If a teller is free, they are served immediately; otherwise they wait. The queue is fair and the bank does not hire more tellers just because the line is long — the number of windows is a design decision.
Now imagine a customer who needs a form signed by a teller in another branch. That teller is also serving a queue. If the first teller waits for the second while holding the customer's paperwork, the first window is blocked for the duration.
Where the analogy breaks#
A bank teller cannot serve two customers at once, and neither can a Binder thread — but a Binder server can serve many calls at once, up to its pool size. The failure mode is not a single blocked queue; it is gradual starvation, where the service still answers but every answer is slower than the last because more threads are parked in a wait.
The big picture#
Text description of this diagram
The Binder transaction sequence again, with two modes. In synchronous mode the client thread occupies a server pool thread and blocks until the reply. In oneway mode the client returns immediately, but the server still needs a pool thread to run the method, and the reply path is absent. The diagram makes clear that oneway removes the client's wait, not the server's work.
Switch between the two modes and look at the server side. The number of server threads consumed is
the same in both. That is the point most performance advice about oneway gets wrong.
Deep dive#
How the pool works#
A process that hosts Binder nodes needs a pool of threads to service incoming transactions. The libbinder default is 15 threads. Threads are spawned on demand as transactions arrive, up to a configured maximum, and once spawned they stay alive until the process ends.1
The maximum can be changed with setThreadPoolMaxThreadCount. A larger pool reduces contention when
many clients call at once, at the cost of more idle threads. A smaller pool saves memory and makes
the server easier to reason about, but queues earlier.
Two details follow from "spawned on demand":
- The first call to a cold service pays for thread creation.
- The pool does not shrink, so a burst of traffic permanently raises the process's thread count.
Re-entrancy and the lock rule#
Synchronous Binder calls can nest. If a client calls a service, and the service calls back into the client's process, the callback can be handled by a different thread in the client — and that thread may need a lock the original caller is holding. The original caller is blocked waiting for the service, the service is blocked waiting for the callback, and the callback is blocked on the lock.
This is a deadlock that a stack trace describes as three unrelated threads, and it is the single
most common Binder design error. The rule is simple: do not hold a lock while making a Binder
call, including a oneway call, because the receiver can still call back into you before the
method returns.
Priority inheritance makes the situation more subtle rather than less. A server thread running a call from a high-priority client is boosted to that client's priority for the duration, so the work is not starved by background threads. That is good for latency and bad for reasoning: the same method may run at different priorities depending on who called it.
Priority inheritance#
Binder supports priority inheritance on synchronous calls. When a high-priority thread blocks on a
transaction to a server, the server thread handling that transaction inherits the caller's priority.
The purpose is latency: a UI-thread call to system_server should not wait behind a background
thread doing bookkeeping.
The practical implications:
- A service cannot assume its threads run at a fixed priority.
- A long-running method called by a high-priority client can starve lower-priority work in the same process.
- Priority inversion — a low-priority thread holding a lock a high-priority thread needs — is mitigated but not eliminated, because the lock holder is not necessarily in the transaction chain.
The cost model#
A Binder call is cheap, but "cheap" has a shape. The costs, roughly in order of size:
- The transaction copy. One copy of the payload, proportional to its size. Large payloads are the expensive case.
- The scheduling round trip. Two context switches, one to deliver and one to reply.
- The thread wakeup. Waking a pool thread and letting it find the transaction.
- The marshalling. Reading the Parcel and constructing objects on the far side.
For a small call — an integer, a string, a boolean — items 1 and 4 are negligible and the round trip dominates. For a call carrying a list of a thousand objects, item 1 dominates and the fix is not a faster Binder but a different design: batch, page, or pass a file descriptor.
oneway: what it does and does not buy#
oneway removes the reply wait. The client thread continues immediately after the driver has
accepted the buffer. That is a real win when the client is a UI thread and the work is fire-and-
forget.
It does not:
- Give the server more threads. The method still runs on a pool thread.
- Make the call unordered. Calls from one node to one target are serialised.
- Guarantee non-blocking. The receiver's async buffer is bounded; when it fills, the driver blocks the sender.
The last point is the one that bites. A burst of oneway calls to a service that is slow to drain
its queue will eventually block the sender, and it will do so at a point unrelated to the call that
filled the buffer.
Freezing changes the rules#
Android 10's cached-app freezer adds a state that Binder has to reason about. A frozen process is alive but gets no CPU.
- A synchronous call to a frozen process will not be allowed to hang: the system kills the frozen target, and the caller gets a failure rather than an indefinite wait.
- A oneway call is buffered until the target unfreezes; buffer overflow can crash the recipient, and the buffered call may be stale when delivered.
For a client, that means a call can fail because the target was frozen, not because anything is
wrong with it. RemoteCallbackList has policies for exactly this, and IBinder can report freeze
state changes.2
How it evolved#
| API | Version | Released | What changed |
|---|---|---|---|
| 1 | 1.0 | 2008-09-23 | — |
| 8 | 2.2 Froyo | 2010-05-20 | — |
| 9 | 2.3 Gingerbread | 2010-12-06 | — |
| 10 | 2.3.3 Gingerbread | 2011-02-09 | — |
| 12 | 3.1 Honeycomb | 2011-05-10 | — |
| 14 | 4.0 Ice Cream Sandwich | 2011-10-18 | — |
| 17 | 4.2 Jelly Bean | 2012-11-13 | — |
Thread-pool behaviour has been stable for a long time; what changed around it is policy. Background
execution limits made oneway broadcasts less useful as a wakeup mechanism, and the freezer added a
third state between alive and dead.
Under the hood#
- ProcessState.cppthread pool setup and the default maximum
- IPCThreadState.cppthe loop each pool thread runs
- Binder.javathe Java side, including calling-UID helpers
- RemoteCallbackList.javacallback registration with death handling
Curiosity corner#
- The default pool is 15, not 16. The number is a libbinder default and can be changed per
process with
setThreadPoolMaxThreadCount. - A
onewaycall can block. Flow control is enforced by the driver, and it does not care that you marked the method asynchronous. - Priority inheritance runs the other way. The server thread inherits the caller's priority, which is why a service's own assumptions about scheduling can be wrong.
Try it#
adb shell ps -T -p $(adb shell pidof system_server) | grep Binder | headThread names follow Binder:<pid>_<n>; the count and PIDs vary.
adb shell kill -3 $(adb shell pidof com.example.app) && adb logcat -d | grep -A 12 "Binder:"dumpstate output for one thread, abridged. Thread ids and addresses differ per process.
Check yourself#
Check yourself
Why does a Binder transaction copy the payload only once?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsBinder threadingsource.android.comaccessed 2026-10-27↩
- docsBinder freezer and cached app freezingsource.android.comaccessed 2026-10-27↩
- docsBinder overviewsource.android.comaccessed 2026-10-27↩
- docsProcesses and threads overview | App qualitydeveloper.android.comaccessed 2026-10-27↩