Live Users, Zero Ad Requests
An ad-supported app with its ads switched off does not crash. It just earns nothing, and in my case every release check passed. Three cases from my own apps, and how to tell them apart.

About 56 people were using the newest release of one of my Android apps. AdMob's count of ad requests from that release was 0.
It was not a low number, and it was not a poor fill rate. The app was never asking for an ad.
This failure is expensive because none of my release checks reported it. An ad-supported app with its ads off does not crash (the kind of failure a crash dashboard never counts), and a missing banner is not something a user complains about. This release had passed every check I had. The only signal was a number on a revenue dashboard, and the build was nine days old before I looked at it.
I can't tell you what those nine days cost, because an app that never requests an ad produces no data to estimate from. What I can show is where the fault was, and why every check let it through.
AdMob shows 0 ad requests while the app has active users
The usual suspects for missing ads are the consent dialog and app-ads.txt, so I started there. Neither could explain a zero. That build's consent layer always permitted requests. The app-ads.txt file was live on the developer website, and a wrong app-ads.txt lowers fill. It does not stop an app from asking.
So I stopped reading source code and opened the thing users had actually installed. I pulled the bundle Google Play was serving, hashed it, and compared it with the file on the build machine.
| Check | Result |
|---|---|
| Bundle served by Play vs. bundle on the build machine | Same SHA-256. Byte-identical. |
| AdMob application ID in the shipped manifest | ca-app-pub-0000000000000000~0000000000 |
Any ca-app-pub- string in the compiled code | None |
ads.properties on the build machine | Absent, in the main checkout and all four worktrees |
| Ad IDs passed by the CI release workflow | None |
What the store was serving had no real ad ID anywhere in it. The source code was correct; the build that shipped was not.
The real IDs lived in a git-ignored file called ads.properties. That release was built on a second machine that had never been given the file. The build script handled a missing ID like this:
if (value == null) return // absent → explicit-disable path, not an error
A missing secret did not stop the build. It quietly changed the product. With no IDs, the release compiled with ADS_ENABLED=false, and because that flag was a constant, the compiler removed the ad code altogether. The Mobile Ads SDK was never initialised. The build was green, every release gate passed, and the upload succeeded.
The fix is small. A shipping release now refuses to build unless real IDs are present, and the only way to ship without ads is an explicit switch that prints a warning:
Refusing to build a shipping release without real AdMob IDs:
ADMOB_APP_ID is not configured (ads.properties, -PADMOB_APP_ID, or env ADMOB_APP_ID)
The second half matters more. After the bundle is built, a check opens the produced .aab and reads the application ID out of the packaged manifest and the banner unit ID out of the compiled code. Checking the inputs tells you what you meant to build. Opening the output tells you what you built.
Ads not showing in an iOS release build
I had already seen this fault once. That is the part I'm least pleased with.
Days earlier, on a different app, a tester working through a checklist on a real iPhone marked one row as failed: no banner ad. The device log had no Google Mobile Ads lines at all.
The ad code was identical to the previous build, which had shown ads. What differed was where the archive was made. It had been built in a fresh git worktree, and a fresh worktree contains only tracked files. The ad IDs were in a git-ignored AdsSecrets.xcconfig, so in that folder the file did not exist and the Release configuration fell back to ADS_ENABLED=NO.
That one never reached users. I held the build and added a Release-only build phase that fails on a missing, malformed or sample ad ID.
Then I fixed it in that app and moved on. I did not go and look at the others. One of them had the same default, had already been built without its file, and was live. It took a zero on a dashboard to send me back. The same guard has since gone into four more of my Android apps.
Which AdMob number tells you where the fault is
Both cases looked like "no ads" to a person holding the phone. In AdMob they look nothing like the last case below, and the difference tells you where to start digging.
| What AdMob shows | What it meant in my apps | Where I'd look first |
|---|---|---|
| Active users, 0 requests | The app never asks for an ad | The shipped build: ad IDs and the ads-enabled flag inside the artifact |
| Requests, 0% match rate | The app asks and AdMob returns nothing | App status in AdMob: limited ad serving, store link, app-ads.txt |
| Requests appear only after the app is restarted | A consent step failed once and was never retried | The consent code path, with the network off on first launch |
Requests are the app's side of the conversation. Match rate is AdMob's. The table is a place to start, not a diagnosis.
The third row is from a different app again. Its consent manager set a one-shot "already asked" flag, so a consent request that failed on a bad connection left ads off until the process was killed. Failed attempts are now retried on the next screen.
AdMob match rate 0% and "No ad to show"
With real IDs in the build, the next release went out to internal testers, and the banner slot was still blank.
The obvious suspect was the ad code I share between apps. The first obstacle was that the release build printed nothing. The shrinker configuration removed informational and warning log calls from release builds, a common size optimisation, and it had removed the ad code's own diagnostic trace with them. I was looking at a blank slot in a build that had been told to say nothing about it.
Once those lines were kept, the log told the whole story in four steps: consent resolved, SDK initialised, banner requested with the correct unit ID, and the result failed with error code 3, NO_FILL. An iOS app had reported the same thing in its own vocabulary: GADBannerView error code 1, "No ad to show." In that app the banner slot collapsed when no ad loaded, which is kind to users and is also why nobody had seen an error.
To rule out my own code, I ran an unmodified release build of a sibling app on the same iPhone. It got no fill either.
The rest was in the AdMob console. The Android app was approved and ready, so its no-fill was only that: AdMob had no ad for that request. iOS was different. One iOS app showed 24 requests in a week and a 0% match rate, while its Android twin matched 27%. The iOS apps at 0% all carried the same status, "Requires review", "Limited ad serving", "Add store to lift limit", because none had been linked to a live store listing yet. The linked apps filled. Limited serving is not zero serving, so I treat the missing store link as the best-supported explanation and not a proven one.
The shared ad code needed no fix. What changed was around it: the log lines stay in release builds, and linking the store listing became a step in each release. I also changed what counts as a pass in QA. The banner row now passes on log evidence, meaning a request with the right unit ID and an SDK result code, plus a slot that does not cover content. A no-fill on a test device is not a failure. Before that change, a healthy build could fail QA over an empty slot, and a build with ads compiled out could pass because an empty slot looked normal.
Check the build that is actually in the store
If your ad revenue has dropped to nothing and nobody can say why, the first question is not about AdMob. It is whether the build in the store contains your ad ID at all. That takes two commands against the release artifact, not the source:
# Android: read the ad ID out of the bundle you uploaded
bundletool dump manifest --bundle=app-release.aab | grep ads.APPLICATION_ID
# iOS: read it out of the unzipped IPA
plutil -p Payload/YourApp.app/Info.plist | grep GADApplicationIdentifier
In my app a string of zeros was the placeholder the build wrote when ads were off. An ID containing 3940256099942544 is Google's sample publisher ID: the app may show ads, but none of them are yours.
If the ID is there and requests are still zero, the fault is further in, and that is a longer conversation. Whether your release checks can be trusted at all is a separate question, covered in how to know an audit actually found nothing. It's the kind of problem I take on: start from what the store is serving, and work backwards to the line that decided it.
Ramiz Raja is a senior mobile developer and technical lead with 12+ years shipping and rescuing production apps on Android, iOS, and Flutter. More at codebyramiz.com.
Keep reading
- Google Play Target API Level Requirement: A Checklist for an App Nobody Has TouchedWhat Play Help requires, what changes when you raise targetSdk to 36, and the order that keeps a neglected app shippable.
- Twelve Cores, Seven Devices, One DiskGit worktrees keep parallel agents out of each other's files. They do nothing about the CPU, the test phones, the disk or the run that hung seven hours ago. Those are the parts that actually broke.
- When Your Coding Agent Hits a Rate Limit: A Fallback Ladder That Keeps WorkingA rate limit, a stalled run that already finished the work, and a pull request that still fails review need three different responses. Here is the ladder I use to tell them apart.