Why our banner ad slot was empty: a week of logging load failures, and 71% were "no fill"
The bottom banner in AdDeck, our AdMob earnings app, was often empty, and we had no records to say why. We started logging every ad load failure with its reason, and after a week, 71% of banner failures were NO_FILL. What we logged and how, what we checked on a device first, and why we made banner retries longer and then shorter again.
The banner was often empty
AdDeck is an app for checking AdMob earnings, and it has one banner ad at the very bottom of the screen. On the developer's phone, that slot was often empty.
When we opened GA4 to explain why, there wasn't a single ad load failure on record. When we cleaned up failure logging across the app on October 16, the ad code had been left out. The AdMob report tells you what percentage of requests got an ad, but not what happened in any particular session.
What we logged
When an ad fails to load, the app now sends the SDK's error code and message to GA4 as one line, code:message (version 1.0.58, October 19).
| Event | When |
|---|---|
ad_banner |
The bottom banner failed to load |
ad_native_start |
The native ad at app start failed to load |
ad_native |
The native ad in the exit popup failed to load |
ad_init |
The ad SDK threw during initialization |
ad_consent_info · ad_consent_form |
Ad consent info or form failed |
giveup |
The short retries all failed too |
Two rules:
- Each value is sent only once per session. If the banner fails for the same reason several times, there is one event.
CANCELLEDis not logged. When our retry sends a new request, the SDK cancels the earlier request it was still holding, and that earlier request's callback getsCANCELLED. That isn't a failure. It's a signal we caused, and counting it would inflate the failures.
We triggered the failures on a device first
Before shipping, we forced failures in a debug build to check that the events actually arrive.
- With the network off,
ad_consent_inforecorded2:Error making request.. On the ad loading side, no failure callback came right away, so nothing was logged. - With the ad unit ID changed to one that doesn't exist, both the banner and the native ad recorded
NETWORK_ERROR:HttpError(responseCode=400…). A wrong ID comes back asNETWORK_ERROR, notINVALID_REQUEST. That's why you need the message too, not just the code name.
One week of results
These are records from 110 users on versions 1.0.58 to 1.0.63, from October 19 to the morning of October 26.
Bottom banner (41 failure events · 18 users)
| Reason | Events | Share |
|---|---|---|
NO_FILL (No fill.) |
29 | 71% |
NETWORK_ERROR (host not found, connection failed, timed out) |
8 | 20% |
TIMEOUT (waiting for the JavaScript engine to load) |
3 | 7% |
INVALID_REQUEST (Ad size will not fit on screen.) |
1 | 2% |
Native ads
| Placement | Failure events | NO_FILL | Network | TIMEOUT |
|---|---|---|---|---|
| Exit popup | 31 | 68% | 23% | 10% |
| App start | 16 | 44% | 38% | 19% |
The network column also includes HTTP 504 responses (3 in the exit popup, 1 at app start).
For the same period (October 19 to 25), the AdMob report shows 1,015 banner requests, a 57.0% match rate, and a 92.1% show rate. GA4 counts once per session and AdMob counts every request, so the numbers can't be the same, but they point the same way. The biggest reason the banner was empty wasn't the app. There was no ad to fill it at that moment. When a request did get an ad, 92% of them actually appeared on screen.
The network errors included these shapes:
Unable to resolve host "googleads.g.doubleclick.net": the ad server's address couldn't be foundFailed to connect to googleads.g.doubleclick.net/0.0.0.0: the ad server's address had been replaced with0.0.0.0. Ad-blocking DNS commonly answers this way, but we didn't check that device's settings.SocketTimeoutException: timeout: it connected, but the response came too late
There were 10 ad consent info failures, and 8 of them were the same 2:Error making request. we saw in the network-off test.
We made retries longer, then shorter
Banner retries had already changed several times before this logging.
| When | On failure |
|---|---|
| First version, September 6 | No retry |
| September 24 | 5 tries, 2 seconds apart |
| October 21 | 5 tries, 3 seconds apart, then every 30 seconds |
| October 23 | 2 tries, 3 seconds apart, then stop |
What we confirmed in the logs on September 25 became the baseline. The auto refresh we had turned on in the AdMob console keeps running even after a failed load: about 110 seconds after the last NO_FILL, the banner filled. If the app also asks again every 30 seconds, the app is doing a second time what the SDK is already doing. So the app now only covers the first few seconds of empty space and leaves the rest to the SDK's refresh.
The logs also show how much the short retries rescue. In the same period, 24 banner events say the short retries all failed (21 giveup, plus 3 slow from the October 21 version). The 41 failure events are one per reason per session, so the banner failed in at most 41 sessions. That means in more than half of the sessions where the banner failed once, asking again a few seconds later still didn't get an ad. NO_FILL rarely changes within a few seconds.
What we don't know yet
- Which users
NO_FILLconcentrates on. We tried splitting by country, but many users have no country in GA4, so we couldn't yet. - The one
INVALID_REQUEST. The message says we asked for a banner bigger than the screen. With only one case, we haven't looked into the cause. - Two
NoSuchElementExceptionconsent info failures. They appeared only in 1.0.61 and were thrown inside the SDK.
Takeaways
- When an ad slot is empty, log the failure reason before trying to fix anything. If most of it is
NO_FILL, the problem is on the ad demand side, not in the app code. - Don't trust the error code name alone; log the message too. A wrong ad unit ID can come back as
NETWORK_ERROR. - Don't count requests that your own retry cancelled (
CANCELLED) as failures. - Sending each value once per session keeps the event volume small.
- If auto refresh is on, keep the app's retries short and leave the rest to the SDK.
- GA4 event counts and AdMob request counts measure different units. When you look at them together, write down what each one counted.
The app in this post is AdDeck, our AdMob earnings app (Google Play).