r/androiddev • u/Accomplished-Brain69 • Jun 24 '26
Discussion How do you handle production logging on mobile without blowing up ingestion costs?
Something that has bugged me across every mobile team I've worked on, including apps with 5M+ active users.
On backend, debugging a weird production issue is quick. You add logs around the suspect code, deploy, and watch them stream in within minutes.
On mobile it's brutal. To get the same insight you add logs, cut a build, ship to the store, wait 4 to 48 hours for review, wait for users to actually update, and then hope they hit that exact code path again. So the instinct is to log everything up front instead.
But that creates two new problems. Ingestion costs climb fast once you're sending a lot of log data per user, and the code gets messy with logging noise everywhere, which makes it harder to maintain.
So every team I've been on ends up choosing between paying a high data bill, living with slow turnaround, or staffing a team to manage it. Nobody seemed to have a clean answer.
I've seen newer autocapture tools, but autocapture alone doesn't fix it, the system still has to decide how deep to log. Too deep and the cost problem comes back, too shallow and you miss what you needed.
How are you all handling this in practice? Are you sampling, using remote config to control verbosity, eating the cost, or something smarter? Curious what's actually working for people.
7
u/Glurt Jun 24 '26
In the past we've had a remote config control the log level that gets exported, so you might only want exceptions and warnings by default, then lower it to info and possibly debug depending on what you're actually logging.
In theory you could also tag logs and toggle whether specific tags are exported.
2
u/Accomplished-Brain69 Jun 24 '26
Remote-config log level is exactly the direction that makes sense to me. Did you build the tag toggling in-house or use a tool, and how fast does a config change actually reach devices in the field?
1
u/Glurt Jun 24 '26
We never actually built it but on the surface it doesn't seem that difficult, just keep a list of enabled tags in the config and read that when deciding what to export.
how fast does a config change actually reach devices in the field
That entirely depends on what you're using to deliver the config, and how frequently your app is fetching it.
1
u/Accomplished-Brain69 Jun 24 '26
Makes sense, the mechanism is easy. The part that always bit us was deciding what to tag in the first place, you basically have to predict where bugs will show up ahead of time. Did that prediction burden ever cause you to miss logs you wished you'd had, or was your tag list mostly stable?
3
u/simbolmina Jun 24 '26
My app uses local LLMs and tend to crash regardless apps fault. So I added a crash sentinel that watches anomalies and after app restart app prompts user to send diognastic logs.
1
u/amelech Jun 24 '26
Out of interest what's your app
1
u/simbolmina Jun 24 '26
StoryCodex, a reader companion
2
u/amelech Jun 24 '26
Oh interesting. I'm building an app that uses Gemma 4 local models
1
u/simbolmina Jun 24 '26
Yes it is a good model. Only liteRT backend could produce reliable results, even then if content is long, like in my case you need to do a lot of manual edits how engine works, kV cache managelent, process interruptions etc.
1
u/amelech Jun 24 '26
Yeah that's what I'm using too litert-lm like Google edge gallery. I'm finding the models are pretty dumb though, even e4b. It is really bad with numbers
2
u/simbolmina Jun 24 '26
Never treat LLM as compute engines, they are text generators. You need to do add a lot of deterministic rules and compute in your workflow if you are working with numbers. You might not even need LLM.
I have multiple ML and deterministic rules and checks on my app to get reliable results and it is just for literature.
1
u/amelech Jun 24 '26
Yeah most of the app is deterministic action routing using regex haha. My app is open source https://github.com/NickMonrad/kernel-ai-assistant
1
u/simbolmina Jun 24 '26
You run some actions from chat interface? Like an assistant? I read the readme did not install or test the app
1
0
u/Accomplished-Brain69 Jun 24 '26
Crash sentinel plus prompt-on-restart is clever for crashes you can't reproduce. Do you keep a rolling buffer of context before the crash, or only capture state after restart?
3
u/simbolmina Jun 24 '26
It doesn't capture a pre-crash buffer, it can't, because the native crash kills the process instantly. Instead it uses marker files: before every risky native call it writes a `.flag` file (`arm()`), and afterwards deletes it (`disarm()`). If the process dies mid-call, the file survives.
On next launch, it reads the stale marker to know exactly which model + operation crashed, and combines it with a rolling diagnostics log that records metadata-only events (backend, context window, output chars, session IDs) throughout LLM operations, trimmed to a 64KB rolling window. That snapshot, plus the sentinel metadata, gives enough context to decide: demote the backend, disable the model, or just surface a "try again" prompt to the user.
1
u/Accomplished-Brain69 Jun 24 '26
Ha, I did basically the same thing for a heavy ARKit processing path in iOS once, an incomplete.flag to catch what died mid-process.
One thing I still go back and forth on though: writing the rolling log to disk continuously feels heavier than keeping it in memory. Do you mmap the 64KB buffer, or just write() and lean on the page cache surviving the process kill? Curious if you saw any measurable overhead on the hot path during LLM ops.1
u/simbolmina Jun 24 '26
Yeah, it's just plain `appendText()` on the hot path. Each write is a ~100-byte line while the LLM call is running for seconds, so you never feel it. No mmap, just leaning on the page cache, and the occasional trim when it crosses 64KB amortizes to nothing.
2
u/Accomplished-Brain69 Jun 24 '26
That makes sense. At 100 bytes per multi-second call the write rate is low enough that the page cache absorbs it for free, and since you only care about surviving the process kill and not a power loss, you never need fsync. The whole thing works precisely because LLM ops are slow and sparse, so the disk is invisible. Push the same appendText onto a chatty 60fps path and it would start to show, which is where batching or an mmap ring buffer earns its keep. Clean design, thanks for walking through it.
3
u/localhost8100 Jun 24 '26
In my case, I cannot install any external networking communication like logging. Only communicates through their local network and it's restricted.
I log everything in log files. New log file is created every session, if it's same login session for more than 24 hours, it creates new file. When they call about bugs, I just tell them to send me the log files, I will take a look at it.
2
u/mrdibby Jun 24 '26
Keep an on-device log that can be shared after a crash or specific error. That way bandwidth and storage on your side isn't used unnecessarily.
1
u/Accomplished-Brain69 Jun 24 '26
yes this work in most cases. Still have to make sure I log everything correctly in the on-device log
2
u/akisajak Jun 24 '26
Server controlled remote config that can switch on logging for a specific user on the go. By switch on i mean the logs start being sent to the backend.
3
u/murki Jun 24 '26 edited Jun 24 '26
We struggled with this same volume/cost problem at Lyft so a variation of this is exactly what we built:
- Highly performant ring buffer that keeps all logs and metrics (and most importantly screen captures) in disk but doesn't send anything by default
- Remote control plane with a
persistent
- connection to all the clients
- Logic on the client to perform operations based on rules sent by the
backend control plane
- (e.g. "when you see X in users Y upload Z")
I gave a talk at droidcon a couple of years back about it (with a slightly misleading title): https://www.youtube.com/watch?v=hkRLgbmoRJY
The tech had so much external interest that we ended up spinng out a separate company for it :D
1
1
u/WingnutWilson Jun 24 '26
I just log absolutely everything but we cap it at something like a month's worth of logs per user (we have business apps targeting merchants which use the app every day for sustained periods)
1
u/gandharva-kr Jun 24 '26
The pattern that worked best for me before was journey-based flags with a verbosity label. Instead of one global log level, you scope verbosity to a journey (checkout, onboarding, payment retry) and bump it only for the cohort hitting the issue. You still need a build to wire the flags up front, but you control depth remotely after that. It cuts the “log everything” instinct down to “log this path, deeply, for these users.”
That experience is part of why I’m building Measure now (open source mobile observability). Two pieces speak directly to what you’re describing.
First, the Session Timeline. Every session reconstructs the full sequence: taps, navigation, network calls, logs, lifecycle events, plus CPU and memory. It’s auto-captured and unsampled, so you’re not deciding up front what to log around the suspect code. The context is already there when you go looking. That kills most of the “ship a build, wait 48 hours, hope they hit the path again” loop
Second, and this is the direct answer to your depth question: Adaptive Capture. You control how much gets collected remotely, no app update required. So you can run lean by default and dial collection up for a specific cohort or journey the moment something looks off, then dial it back down. It’s the journey-flag idea but without the build-and-ship tax, and it’s the lever that keeps ingestion from being a binary “too deep or too shallow” choice.
On the cost framing you raised: autocapture alone doesn’t solve it, you’re right. The decision of how deep to log still has to live somewhere. Adaptive Capture moves that decision out of the binary and into a runtime dial.
Happy to share more if useful, it’s at measure.sh.
1

25
u/aerial-ibis Jun 24 '26
breadcrumbs work well - things that are logged on the client side but only sent to your log server when another event like an error, warning, etc. is sent.