---
title: "eBPF for Mobile API Observability: Zero-Touch Tracing from Android to Your Backend"
published: true
description: "Use eBPF TC hooks and uprobes on OpenSSL to trace Android OkHttp requests end-to-end at the kernel level — no code changes, no instrumentation overhead."
tags: [android, mobile, api, architecture]
canonical_url: https://mvpfactory.co/blog/ebpf-mobile-api-observability
---
What We Are Building
By the end of this tutorial, you will have a mental model — and working probe code — for capturing Android OkHttp requests at the kernel level on your backend, correlating them with W3C traceparent headers, and assembling end-to-end flame graphs that include network transit, TLS handshake, and kernel queueing time. Zero application changes required.
This is the observability gap nobody talks about. A mobile user reports 800ms of slowness. Your APM shows green. Your backend p99 looks fine. The latency is hiding across your Android HTTP stack, the network, TLS negotiation, and server-side queueing — and nothing stitches those together by default.
eBPF changes this entirely.
Prerequisites
- Linux backend (kernel 5.8+ recommended for ring buffer support)
- OpenSSL or BoringSSL as your TLS terminator (Nginx, Envoy, or direct)
-
bpftraceinstalled for prototyping - Android app using OkHttp (any recent version)
- Basic familiarity with C and distributed tracing concepts
Step 1 — Understand Your Three Attachment Points
eBPF lets you run sandboxed programs in the Linux kernel with near-zero overhead. For mobile API tracing, three hooks matter:
| Attachment Point | What It Captures | Overhead |
|---|---|---|
| XDP | Raw packets at NIC driver level | Minimal — pre-kernel stack |
| TC hooks | Ingress/egress at socket buffer level | Low — post-routing |
| uprobes on OpenSSL/BoringSSL | Plaintext data post-TLS handshake | Low — userspace probe |
XDP gives you packet-level metadata fastest. TC hooks give you structured socket buffer access. But here is the gotcha that will save you hours: for TLS traffic — which is everything from Android — uprobes on the TLS library are where the real work happens. The kernel never sees plaintext until after the handshake, so XDP and TC alone cannot read your HTTP headers.
Step 2 — Attach a Uprobe to SSL_read
Android's HTTP stack (OkHttp backed by BoringSSL) encrypts everything before it hits the wire. On your backend, attach a uprobe to SSL_write and SSL_read in your OpenSSL or BoringSSL shared library. These fire after the handshake, at the exact moment plaintext is passed into the TLS engine.
SEC("uprobe/SSL_write")
int trace_ssl_write(struct pt_regs *ctx) {
int fd = (int)PT_REGS_PARM1(ctx);
void *buf = (void *)PT_REGS_PARM2(ctx);
int num = (int)PT_REGS_PARM3(ctx);
// Read HTTP headers from buf, extract trace ID
// Emit correlation event to ring buffer
return 0;
}
From buf, you extract the traceparent or X-Request-ID header that OkHttp injected. That is your correlation anchor — captured at the kernel boundary with no middleware involved.
Let me show you a pattern I use in every project: prototype this with bpftrace before building a production pipeline. You can attach to SSL_read in minutes and print header bytes to stdout. Validate your probe works before writing a single line of Go or Rust collector code.
Step 3 — Wire the Full Pipeline
Here is the minimal setup to get this working end-to-end:
Android side: OkHttp's Interceptor interface injects a W3C traceparent header on every outbound request. Standard distributed tracing, nothing exotic.
Backend pipeline:
- TC ingress hook captures the incoming TCP segment — records arrival timestamp, source IP, connection tuple
- Uprobe on
SSL_readcaptures decrypted HTTP headers and extractstraceparent - kprobes on socket operations track kernel queueing time before your application thread picks up the request
- All events flow into a perf ring buffer, consumed by a Go or Rust userspace daemon that assembles the full trace timeline
The result: a flame graph showing network transit time, TLS handshake duration, kernel queueing latency, and application processing time — all correlated to the exact Android request that triggered it.
Gotchas
Do not bundle the collector into your application process. Deploy it as a sidecar. Keeping eBPF programs and ring buffer consumers separate means you can update your observability stack independently, and it survives application restarts without losing trace data.
Inject traceparent before you touch any eBPF code. The docs do not mention this explicitly, but without the correlation header in OkHttp, your kernel-level capture has no anchor to join against your backend spans. It costs nothing and unlocks everything downstream.
Overhead is not a concern at this scale. eBPF programs run with JIT compilation and verifier-enforced safety. TC hook and uprobe combinations typically run in single-digit microseconds per request — well below any latency budget that matters at mobile scale. Application-level APM agents compound heap allocations and serialization overhead at p99. eBPF does not.
Conclusion
If you are running OpenSSL or BoringSSL on your backend, you already have everything you need to start. Open bpftrace, attach a uprobe to SSL_read, and watch your OkHttp headers appear at the kernel boundary. That moment — seeing plaintext HTTP headers in a kernel probe with zero application changes — is when this approach stops being theoretical.
Full end-to-end flame graphs from Android to your backend, no SDK changes, no instrumentation overhead. That is the gap eBPF closes.
Top comments (0)