AOSP Framework & Internals
5 min read

One-Way Calls

Learn about One-Way Calls.

The Cost of Waiting

By default, Binder transactions strictly block your execution. When your application calls a system service, the kernel puts your calling thread to sleep. It only wakes up when the remote process finishes its work and sends a reply. This blocking architecture guarantees that you have a result before your code continues. But it also introduces a severe vulnerability. If the remote service is heavily loaded or deadlocked, your thread is stuck waiting.

If you make this call on your main thread, your application will quickly trigger an Application Not Responding (ANR) crash. For operations like sending a stream of sensor updates or writing telemetry logs, waiting for a reply is unacceptable. You need a way to dispatch the data and immediately move on.

The Fire-and-Forget Keyword

To solve this blocking problem, Android provides the oneway keyword. You apply this keyword in your AIDL file to either a specific method or an entire interface. It acts as an explicit instruction to the Binder driver that the client does not expect a reply.

Because the caller never waits for a response, the method signature is heavily restricted. A oneway method must always return void. It also cannot contain out or inout parameters, since there is no mechanism to pass modified data back to the caller.

interface ILogger {
    // A standard synchronous call
    int getLogSize();

    // A fire-and-forget asynchronous call
    oneway void writeLog(String message);
}

When the AIDL compiler generates the proxy classes, it flags the writeLog transaction differently. This flag changes how the kernel routes the request under the hood.

Kernel Routing for Asynchronous IPC

The following sequence diagram compares a standard blocking call with an asynchronous one-way call. It helps visualize the difference in thread suspension between the two approaches. Notice how quickly the client resumes execution in the one-way flow.

When you invoke a one-way method, the Binder driver copies your transaction data into the memory buffer of the target process. Instead of suspending your thread, the driver immediately wakes it back up. Your application continues executing the next line of code instantly. Meanwhile, the kernel schedules a thread in the remote process to handle the payload in the background. You completely decouple the performance of your client from the workload of the server. This decoupling is fast, but it introduces a subtle synchronization trap.

The Serialized Execution Trap

Because one-way calls return instantly, a fast client could easily fire thousands of requests per second. If the kernel spawned a new server thread for every request, a single spamming client would rapidly exhaust the remote thread pool. To prevent this denial of service, the Binder driver handles asynchronous calls uniquely. It serializes all oneway transactions targeting the same IBinder object.

The kernel queues the requests and only dispatches one transaction to the server at a time. The second call waits in the driver queue until the server finishes processing the first one. This guarantees strict ordering for messages sent to a specific binder instance.

Common Mistake: Developers often assume oneway means parallel execution. It actually means asynchronous but strictly ordered execution. If your oneway method performs a long database write, it will stall all subsequent oneway calls to that specific interface inside the kernel queue.

This strict ordering protects the server thread pool, but it shifts the pressure to system memory.

Buffer Exhaustion and Missing Guarantees

You gain speed with asynchronous IPC, but you lose your safety nets. Since the client does not wait for execution, it cannot catch exceptions thrown by the remote process. If the server throws a RemoteException or crashes while parsing your data, your client remains blissfully unaware. You have no confirmation that the work actually succeeded.

A more severe limitation involves memory management. Every process has a limited Binder buffer, which is typically capped at 1MB. Normal synchronous calls naturally throttle themselves because the client blocks until the server finishes. Asynchronous calls lack this natural throttle. If your client fires data faster than the serialized server thread can process it, the kernel queue grows and fills the server buffer. The kernel will reject the next transaction, causing your client to crash with a TransactionTooLargeException.

You can fire a message into the void and hope the remote process is alive to receive it. But hope is not a valid engineering strategy. To build resilient asynchronous architectures, you need a deterministic way to know when a remote process abruptly terminates.