One morning I opened the AdMob dashboard as usual and found yesterday's revenue greyed out. A note explained that the earnings had been adjusted as invalid traffic. My stomach dropped a little.
Nothing came to mind right away. I hadn't done anything improper, yet the numbers had clearly been reduced. That state of "my revenue is shrinking and I don't know why" is, honestly, the most uncomfortable moment I run into with AdMob.
When you run several apps solo, you pass through this kind of scare more than once. Today I want to share something I've learned from that experience, turned into concrete mechanics: most invalid traffic is preventable.
Invalid Traffic Isn't Only About Fraud
The phrase "invalid traffic" tends to conjure images of malicious click farms. That's part of it, but what solo developers actually hit is usually far more mundane and close to home.
In practice, it's things like tapping your own production ad while building the app, handing a test build to family members who casually interact with an ad, or the same device requesting ads an unnatural number of times. Google mechanically deducts impressions and clicks it judges to be "not natural human behavior." Intent doesn't matter; what matters is the pattern.
In other words, most invalid traffic grows out of gaps in your own workflow. Which means that if you close those gaps with a system, most of it simply never happens.
The Most Common Cause: Hitting Your Own Production Ads While Building
When you run an app built with Rork or Expo on your own device and tune it, you display ads over and over just to check the screen. If the production ad unit is live during that, your own taps and impressions blend straight into production data. This is the most likely cause, and the easiest one to overlook.
There are only two countermeasures. First, register your own device as a test device. Second, always switch to a test ad unit in development builds. Wire both in from the start, and you close nearly every path that would let development sessions pollute production data.
With react-native-google-mobile-ads, you register the test device during initialization and select the ad unit ID based on the environment.
import mobileAds, { TestIds, MaxAdContentRating } from 'react-native-google-mobile-ads';
// 1) Register your own device as a test device.
// Even if you point at a production ad unit, this device only receives test ads.
async function initAds() {
await mobileAds().setRequestConfiguration({
// The device ID is printed to the logs on first launch (see below).
testDeviceIdentifiers: ['EMULATOR', 'YOUR_DEVICE_ID'],
maxAdContentRating: MaxAdContentRating.PG,
});
await mobileAds().initialize();
}
// 2) Always split ad unit IDs between development and production.
// __DEV__ is the development flag that Expo / React Native provides automatically.
export const BANNER_UNIT_ID = __DEV__
? TestIds.BANNER
: 'ca-app-pub-0000000000000000/1111111111';
export const INTERSTITIAL_UNIT_ID = __DEV__
? TestIds.INTERSTITIAL
: 'ca-app-pub-0000000000000000/2222222222';What matters here is applying the __DEV__ split in every place that references an ad unit. The classic gap is switching the banner but leaving a production ID on the interstitial. Gather these constants into a single file and have each screen read from it, so that a leftover is far less likely.
To find your device ID, launch the app once while the device is still unregistered as a test device. A line like the following prints to the console; paste that string into testDeviceIdentifiers.
I/Ads: Use RequestConfiguration.Builder.setTestDeviceIds(Arrays.asList("33BE2250B43518CCDA7DE426D04EE231"))
to get test ads on this device.Running Several Apps Means Leaning on Systems, Not Attention
With a single app, careful manual switching is usually enough. But as the number of apps you run grows, relying on your awareness of "which build am I touching right now" becomes fragile. Human attention is the first thing to drop when you're tired or up against a deadline.
So wherever you can protect yourself with a system instead of vigilance, lean on the system. Here are the preventive measures I've found consistently effective across multiple apps, ordered from most to least impactful.
| Preventive measure | Accident it prevents | Impact |
|---|---|---|
| Split ad units with __DEV__ | Hitting your own production ads while building | High (cuts the most common cause at the root) |
| Register your and family devices as test devices | Natural taps leaking in on your or a tester's device | High |
| Centralize ad unit IDs in one constants file | A leftover production ID on just one screen | Medium |
| Frequency capping | Unnatural repeated impressions on one device | Medium |
| Add an ad check to your pre-release checklist | Forgetting to switch back | Medium |
Of these, the highest return across multiple apps, in my view, is centralizing ad unit IDs in a single constants file. Even when the code style differs a little from app to app, fixing the single entry point where ad units are referenced lets you guarantee the __DEV__ split in exactly one place. Each time you start a new app, you just carry that constants file over as a template.
Frequency capping is less about invalid traffic per se and more about avoiding excessive ad exposure in the first place. I've written separately about designing that, including the felt experience on app resume, in Frequency Design for AdMob App Open Ads: Chasing eCPM While Easing the Stress of Every Return.
How to Move When a Warning or Deduction Does Arrive
Even with prevention in place, an unexplained deduction can still show up. Making large, panicked changes at that point only makes the situation harder to read. I try to respond calmly in this order.
First, I recall what I did recently. I list out anything that fits: I added a new ad placement, I expanded a test distribution, I repeatedly checked something on a particular device. Most of the time, this is where the likely cause surfaces.
Next, I stop the path I suspect. If there was a route that let me touch production ads, I revisit the __DEV__ split and test device registration. Waiting and watching while leaving the cause in place just means the same adjustment lands again tomorrow.
Then, if I genuinely believe Google's assessment contains a clear error, I use the invalid traffic inquiry form in AdMob help to describe the situation. The key here is to write plainly about "when, through which action, and what happened," rather than emotion. Adjustments are often reflected and reviewed over several days, so after sending, I wait for the outcome.
Finally, so the same accident doesn't recur, I add one line about the cause and the fix to my pre-release checklist. This is how a system quietly thickens, one incident at a time.
Wrapping Up
For solo developers, invalid traffic is far more likely to grow out of gaps in your own workflow than from a malicious attack. That's exactly why it can be prevented with systems rather than vigilance.
If you move your hands on just one thing today, start by gathering your ad unit IDs into a constants file and splitting production and test with __DEV__. It cuts the most common cause with the least effort.
I'm still learning as I go with my own operations. If this offers a bit of quiet reassurance to someone nurturing their apps the same way, I'd be glad. Thank you for reading.