Binder, the driver
One ioctl, a Parcel, a handle table and a reference count. The Binder driver is a small kernel component that decides how a hundred million calls a day behave when things go wrong.
A Binder call looks like a function call. Underneath it is one ioctl to a character device, a
serialised buffer, and a kernel-side reference count. The gap between those two descriptions is
where every Binder bug lives.
TL;DR#
- The client holds a proxy (
BpBinder), the server holds a node (BBinder), and the driver translates between handles and nodes. - A transaction is one ioctl carrying a target handle, a method code, flags and a Parcel. The driver copies the buffer into the receiver and wakes a thread from its pool.
- Objects sent over Binder are not copied. The driver creates a handle in the receiver and reference-counts the node; when the count reaches zero, the object dies and death notifications fire.
- The per-process transaction buffer is bounded, which is why large payloads throw
TransactionTooLargeExceptioninstead of quietly working. - calls skip the reply but still obey flow control: the driver blocks the sender when the receiver's async buffer is full.
Mental model#
Think of a company's internal courier system.
You do not send documents to a person; you send them to a desk number. The courier company keeps a directory that maps each desk number to a physical desk. When you hand over an envelope, the courier checks your employee badge, writes your name on the envelope, and carries it to the desk. The person at the desk signs a receipt, which comes back to you.
If the person at the desk has left the company, the courier returns the envelope marked recipient gone instead of leaving you waiting. And if you want to let the other desk send things back to you later, you do not give them your desk — you give them a claim ticket that the courier company can redeem on your behalf.
Where the analogy breaks#
The courier analogy implies a company that owns both offices. Binder's guarantee is stronger: the driver is not trusted by either side to be fair, only to be correct. And the "claim ticket" is more than an address — it keeps the original object alive. A Binder handle holds a reference count on the node, so the object cannot be destroyed while someone still holds a handle to it. That is reference counting, not directory lookup.
The big picture#
Text description of this diagram
A playable sequence of a Binder transaction between an app and a system service. The client thread calls a method on a generated proxy, which writes an interface descriptor, method code and arguments into a Parcel. The proxy issues a BC_TRANSACTION ioctl to the binder driver. The driver resolves the handle to a node in the service process and copies the transaction into a buffer in that process's address space. A thread from the service's binder pool wakes, reads the method code, checks the caller and invokes the implementation. In synchronous mode the service writes a reply, the driver wakes the blocked client thread, and the proxy returns. In oneway mode the driver returns to the client as soon as the buffer is queued, and there is no reply stage.
Switch the diagram between Synchronous and oneway and watch which steps disappear. The
inbound half is identical; the difference is entirely in what happens after the server has the data.
That is the right way to think about oneway: it is not a different transport, it is a different
contract about the reply.
Deep dive#
The four objects#
Binder's programming model has a small vocabulary, and almost every confusion comes from mixing up two of these:
| Object | Lives in | What it is |
|---|---|---|
IBinder | client | The interface your code holds; either a local object or a proxy |
BpBinder | client, libbinder | The proxy that knows a handle number |
BBinder | server, libbinder | The local object that receives transactions |
binder_node | kernel | The driver's record of a server-side object |
A BpBinder contains a handle, which is an integer meaningful only in that process. The driver
keeps a per-process table mapping handle to node. Send the same IBinder to two processes and they
get two different handles; the driver knows both refer to the same node.
This is why "pass a Binder object" is cheap. No state is copied — only a reference is created and a reference count is incremented.
The transaction, field by field#
A transaction is an ioctl on /dev/binder. The interesting fields are:
- handle — which node in the target process this is for.
- code — an integer method id. AIDL allocates these; hand-written Binder code picks its own.
- flags —
TF_ONE_WAYis the important one. It tells the driver not to expect a reply. - data — the Parcel buffer: interface descriptor, arguments, and any embedded file descriptors.
- reply — a second buffer the server writes its return value into.
The driver validates the handle, checks that the target process is alive, copies the buffer, and
enqueues the transaction on the target's todo list. Then it wakes a thread. On the way back, the
server calls BC_REPLY with the reply buffer, and the driver wakes the blocked client.
Parcels#
A is a flat buffer with a write cursor and a read cursor. It knows how to
serialise primitives, strings, arrays, IBinder references and file descriptors. Two details matter
more than the rest.
File descriptors are duplicated, not copied. When you write a file descriptor into a Parcel, the driver installs an equivalent descriptor in the receiver. That is how Binder passes a shared-memory region or a socket without ever putting the data itself in the transaction.
The buffer is bounded. Each process has one transaction buffer, and a transaction that exceeds
it fails. The exact size is a per-device configuration rather than a constant you should hardcode;
what matters is that it is on the order of a megabyte and shared by all in-flight transactions in
that process.1 A large Bundle, a bitmap, or a long list sent in
one call can exhaust it, which is what TransactionTooLargeException reports.
Text description of this diagram
Two IPC paths compared. In Binder, a sender's Parcel is copied once: the kernel maps part of the receiver's buffer into the sender and writes the transaction there. In a pipe or Unix socket, the data is copied into a kernel buffer and then copied out into the receiver, two copies. Binder's transaction also carries the sender's UID, PID and security context, and Binder tracks object references across processes, which a socket does not.
Reference counting and death#
The driver maintains a reference count per node. A handle in a client process counts as a reference; so does an entry in the driver's own tables. When the count drops to zero, the node is released.
Death notification is the other half. A client can register a DeathRecipient on an IBinder. When
the process that owns the node dies, the driver fires a callback: the client's recipient is invoked
with the dead binder, so it can clean up, retry, or report an error. This is why a well-written
client can recover from a service crash without polling.
The mechanism is in the kernel, so it works for SIGKILL, for a native crash, and for a process
that exits cleanly. It does not fire for a frozen process — freezing is not death, which is exactly
the distinction the freezer machinery relies on.
oneway, flow control, and the async buffer#
A oneway method returns as soon as the driver has accepted the transaction. There is no reply, no
return value, and no exception from the server — a oneway call cannot throw a remote exception
because there is nowhere to put it.
The subtlety is that oneway calls from the same node are serialised. They may run on different
threads, but never at the same time. If you send two oneway calls, the second cannot overtake the
first. That is a useful guarantee and a common source of surprise for people who assume "async"
means "unordered".2
The second subtlety is flow control. The receiver has a bounded buffer for asynchronous
transactions. When it fills, oneway calls block in the driver until space frees up. A oneway
call is therefore usually non-blocking, not always. The first oneway call to a busy target, or
the thousandth in a burst, can block for a noticeable time.
The freezer#
Modern Android can freeze a cached app: the process stays in memory but gets no CPU. That changes what a Binder call to it means, and the driver is where the policy is enforced. The cached-app freezer was introduced in Android 10 and the policy was made more aggressive in Android 14.unverified(The AOSP freezer page describes the mechanism but does not state the release that introduced it, and this site's version data records only the Android 14 tightening.)
A synchronous call to a frozen process is not allowed to hang. The system kills the frozen process
instead, on the theory that a caller should never block indefinitely on a target that cannot run.
A oneway call is buffered until the target unfreezes; if the buffer overflows, the recipient can
crash, and buffered calls may be stale by the time they are delivered.3
This is the clearest example of the general rule: Binder semantics are not just "call a function somewhere else". They encode the platform's policy about what happens when the other side is not available.
How it evolved#
| API | Version | Released | What changed |
|---|---|---|---|
| 1 | 1.0 | 2008-09-23 | — |
| 8 | 2.2 Froyo | 2010-05-20 | — |
| 10 | 2.3.3 Gingerbread | 2011-02-09 | — |
| 11 | 3.0 Honeycomb | 2011-02-22 | — |
| 12 | 3.1 Honeycomb | 2011-05-10 | — |
| 14 | 4.0 Ice Cream Sandwich | 2011-10-18 | — |
| 17 | 4.2 Jelly Bean | 2012-11-13 | — |
Two rows deserve a note. The freezer changed the meaning of a call to a cached process, and Android 14 froze such apps more aggressively. Later releases also kept tightening what a Binder call may do to memory — the exact rules differ per release, so treat the direction as the stable part.
Under the hood#
- binder.cthe transaction engine: ioctl handling, copy, thread wakeup
- binder_alloc.cthe per-process buffer allocator — where the size limit lives
- BpBinder.cppthe client-side proxy
- IPCThreadState.cppthe server-side transaction loop
- Parcel.javathe Java Parcel API
The allocator file is the one to read if you care about TransactionTooLargeException. The limit
is not a check at the API boundary; it is the size of the buffer the allocator hands out.
Curiosity corner#
- A
onewaycall can still block. Flow control is the reason, and it is enforced by the driver, not the service. - Handles are per-process. Printing a handle from one app tells you nothing about another app's handle for the same service.
- The driver is upstream. Binder lives in the mainline Linux tree, which is unusual for an Android-specific mechanism and the reason it survives kernel upgrades.
Try it#
adb shell dmesg | grep -i binder | head -20Log lines differ by kernel version; dmesg may require root.
adb shell ls -l /dev/binder*Major and minor numbers depend on the kernel; the node names are the stable part.
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 overviewsource.android.comaccessed 2026-10-27↩
- docsBinder threadingsource.android.comaccessed 2026-10-27↩
- docsBinder freezer and cached app freezingsource.android.comaccessed 2026-10-27↩
- docsbinderfs — Linux kernel documentationdocs.kernel.orgaccessed 2026-10-27↩