Skip to main content

How to Lift AdMob "Ad Serving Limited" Restrictions Fast

 There is no notification more dreaded by an app publisher than the email from Google AdMob: "Ad serving is currently limited on your account."

In an instant, your fill rate drops to near 0%. Your revenue flatlines. The AdMob Policy Center offers vague reasoning like "Invalid Traffic Concerns" without pointing to specific screens or code blocks. You are left guessing whether you are under attack by a bot farm or if your implementation is flawed.

This is not a waiting game. Waiting for the algorithm to "re-assess" your traffic can take 30 to 60 days. To lift the restriction in under a week, you must proactively sanitize your ad requests.

This guide details the technical root causes of ad limiting and provides a programmatic implementation to throttle requests and eliminate accidental clicks—the two primary triggers for this penalty.

The Root Cause: Why Google Flags Your Traffic

To fix the problem, you must understand the mechanism. Google’s ad fraud algorithms rely on machine learning to detect anomalies. They do not have a human reviewing your app initially. They look for statistical outliers.

The most common trigger for "Ad Serving Limited" is not malicious bot traffic; it is Accidental Click-Through Rate (CTR) inflation.

The CTR Anomaly

Standard banner ads typically see a CTR between 0.5% and 1.5%. Interstitials might range from 2% to 4%. If your app suddenly reports a CTR of 8% or higher, the algorithm assumes one of two things:

  1. Deceptive Implementation: You are tricking users into clicking ads.
  2. Bot Traffic: Non-human actors are clicking ads.

Google’s immediate defense mechanism is to stop sending ads (Limit Ad Serving) to protect their advertisers' budgets while they gather more data.

The Refresh Loop Trap

Another technical trigger is a high ratio of Requests vs. Impressions. If your code requests an ad every time a component re-renders (a common React/Flutter anti-pattern) but only shows it 10% of the time, your "Show Rate" plummets. Low show rates signal low-quality inventory, prompting a limit.

The Solution: Technical Remediation

You cannot simply email Google support. You must deploy code that physically prevents the behaviors triggering the algorithm.

We will implement a Safe Ad Wrapper strategy. This involves two technical interventions:

  1. Strict Frequency Capping: preventing ad requests based on user session time.
  2. Layout Shifts Protection: enforcing safe zones to prevent accidental clicks (mis-taps).

Step 1: Implementing Programmatic Frequency Capping

Do not rely solely on AdMob's console frequency capping. That caps impressions (what users see), but it doesn't always stop the requests (what your app asks for). Sending too many requests that get unfilled hurts your score.

We need a logic layer that ensures we only request an ad when the user is truly engaged.

Here is a modern TypeScript implementation using React Hooks (compatible with React Native/Expo) that manages ad request throttling.

import { useState, useEffect, useRef, useCallback } from 'react';
import { AppState, AppStateStatus } from 'react-native';

// Configuration constants
const MIN_TIME_BETWEEN_ADS_MS = 60000; // 1 minute minimum
const MIN_SESSION_TIME_BEFORE_FIRST_AD_MS = 10000; // 10 seconds warmup

interface AdLimitConfig {
  unitId: string;
  adType: 'BANNER' | 'INTERSTITIAL';
}

export const useSmartAdLimiter = ({ unitId, adType }: AdLimitConfig) => {
  const [canRequestAd, setCanRequestAd] = useState<boolean>(false);
  const lastRequestTime = useRef<number>(0);
  const sessionStartTime = useRef<number>(Date.now());
  const appState = useRef<AppStateStatus>(AppState.currentState);

  // Helper: Check if enough time has passed since app launch
  const isSessionMature = useCallback(() => {
    return Date.now() - sessionStartTime.current > MIN_SESSION_TIME_BEFORE_FIRST_AD_MS;
  }, []);

  // Helper: Check if enough time has passed since last ad
  const isFrequencySafe = useCallback(() => {
    return Date.now() - lastRequestTime.current > MIN_TIME_BETWEEN_ADS_MS;
  }, []);

  useEffect(() => {
    // 1. Monitor App State to pause timers/logic when backgrounded
    const subscription = AppState.addEventListener('change', (nextAppState) => {
      if (
        appState.current.match(/inactive|background/) &&
        nextAppState === 'active'
      ) {
        // App came to foreground: Reset session timer to prevent instant ads
        sessionStartTime.current = Date.now(); 
      }
      appState.current = nextAppState;
    });

    // 2. Logic Loop to validate ad request eligibility
    const validationInterval = setInterval(() => {
      if (isSessionMature() && isFrequencySafe()) {
        setCanRequestAd(true);
      } else {
        setCanRequestAd(false);
      }
    }, 2000);

    return () => {
      subscription.remove();
      clearInterval(validationInterval);
    };
  }, [isSessionMature, isFrequencySafe]);

  const logAdRequest = () => {
    lastRequestTime.current = Date.now();
    setCanRequestAd(false); // Immediate lock
  };

  return { canRequestAd, logAdRequest };
};

Step 2: The "Click Shield" Container

High CTR usually happens because content loads, the layout shifts, and a user trying to click "Next Level" accidentally clicks a banner that jumped under their thumb. This is a policy violation called "Encouraging Accidental Clicks."

You must create a rigid container that reserves space for the ad before it loads.

import React from 'react';
import { View, StyleSheet, Text } from 'react-native';
import { BannerAd, BannerAdSize } from 'react-native-google-mobile-ads';
import { useSmartAdLimiter } from './useSmartAdLimiter';

interface SafeBannerProps {
  adUnitId: string;
}

export const SafeBannerZone: React.FC<SafeBannerProps> = ({ adUnitId }) => {
  const { canRequestAd, logAdRequest } = useSmartAdLimiter({
    unitId: adUnitId,
    adType: 'BANNER',
  });

  if (!canRequestAd) {
    // Return null or a placeholder. 
    // CRITICAL: Do not return an empty View with height 0 if content shifts!
    // Return a transparent view of the expected ad height to maintain layout stability.
    return <View style={styles.placeholder} />;
  }

  return (
    <View style={styles.safeContainer}>
       {/* 
         Top Padding creates a visual gap between content and ads.
         This is physically required to prevent accidental clicks. 
       */}
      <View style={styles.safetyMargin} />
      
      <BannerAd
        unitId={adUnitId}
        size={BannerAdSize.ANCHORED_ADAPTIVE_BANNER}
        requestOptions={{
          requestNonPersonalizedAdsOnly: true,
        }}
        onAdLoaded={() => {
          logAdRequest();
        }}
        onAdFailedToLoad={(error) => {
          console.error('Ad failed to load: ', error);
          // Even on fail, log request to prevent rapid retry loops
          logAdRequest(); 
        }}
      />
      
      {/* Bottom Padding */}
      <View style={styles.safetyMargin} />
    </View>
  );
};

const styles = StyleSheet.create({
  safeContainer: {
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#f0f0f0', // distinct background to separate ad from content
  },
  placeholder: {
    height: 50, // Approximate standard banner height
    width: '100%',
    backgroundColor: 'transparent',
  },
  safetyMargin: {
    height: 10, // 10px hard buffer where no clicks can occur
    width: '100%',
    backgroundColor: 'transparent',
  },
});

Deep Dive: Why This Fix Works

This code directly addresses the metrics Google's AI is analyzing.

1. The Warm-up Period (MIN_SESSION_TIME_BEFORE_FIRST_AD_MS) Bots typically launch an app, scrape it, and leave immediately. By delaying the first ad request by 10 seconds, you prevent ad requests from firing during short, low-quality sessions. This increases your average "Session Duration per Ad Impression," a massive quality signal.

2. Layout Stability (CLS) By reserving space (styles.placeholder) and adding styles.safetyMargin, you eliminate "layout shift." Google detects when users tap an ad immediately after it renders. If the "Time to Click" is < 1 second, it is flagged as accidental. The safety margin physically separates interactive app elements (buttons) from the ad.

3. Throttling Requests The useSmartAdLimiter hook prevents the "waterfall of death." If you use standard navigation in apps, moving back and forth between screens can trigger a new ad request every few seconds. This floods AdMob with requests. By locking the request capability for 60 seconds (MIN_TIME_BETWEEN_ADS_MS), you increase your Match Rate (Filled Ads / Requested Ads). A high Match Rate signals a healthy implementation.

Common Pitfalls to Avoid

While implementing the fix, avoid these errors which can extend the limitation period:

1. Removing All Ads

Do not remove your ad units entirely. AdMob needs traffic data to re-evaluate your account. If you send zero requests, they cannot verify that the traffic is now valid. Reduce the volume, but keep the pipe open.

2. Testing on Live Builds

Never click your own ads, and never test on a production build without adding your device as a Test Device in the AdMob console. Even a single click from a developer's device can trigger a "Self-Click" violation.

3. The "OnFocus" Refetch

A common mistake in React Native/Flutter is placing the ad load command inside a useFocusEffect or onAppear. If a user minimizes the app and reopens it, or switches tabs rapidly, this triggers an ad reload. Always check timestamps before reloading.

Analyzing the Recovery

Once you push this update to the app store:

  1. Link AdMob to Firebase: Ensure your AdMob account is linked to Firebase Analytics.
  2. Monitor CTR: Watch the CTR for the specific ad units. It should stabilize between 0.5% and 1.5%.
  3. Check Match Rate: Your match rate should climb. If it was 20% during the limit, it should creep back toward 90%+.

Conclusion

Lifting an AdMob restriction is an exercise in data hygiene. The "Ad Serving Limited" penalty is a mechanism to force you to clean up your implementation. By programmatically capping requests and enforcing layout safety, you provide the algorithm with the high-quality signals it needs to trust your traffic again.

Implement the request throttling and safety zones immediately. Once Google’s systems detect the stabilized click patterns and higher value impressions, the limit is typically lifted automatically within 5 to 7 days.