Skip to content

Caching, retry and timeouts

Which knobs does AdMob CMP actually expose?

Section titled “Which knobs does AdMob CMP actually expose?”

Everything on this page is per-placement configuration on AdPlacement, so different placements can behave differently in the same app. The four policies and what each one bounds:

The reason these are policies, not flags, is that the library must enforce a few invariants: maxSize >= 1, every TTL finite and positive, the backoff multiplier >= 1. A flag is a single value with no internal consistency; a policy is a small object whose constructor can validate the whole thing at one moment. Constructors throw, defaults are sensible, and a misconfigured placement fails at the construction site rather than at the first load().

PolicyBoundsDefault
AdCachePolicyHow many ads are held, and whether one reloads after a showmaxSize = 1, reloadAfterShow = false
AdExpirationPolicyHow long a cached ad stays usable1h full-screen, 4h app-open, 1h native
AdRetryPolicyHow a failed load is retried2 attempts, 2s initial delay, 30s cap, 2.0 multiplier
AdTimeoutPolicyHow long a load or a presentation hand-off may take30s load, 10s presentation hand-off
AdPlacement(
id = "interstitial_level_end",
format = AdFormat.Interstitial,
adUnitIds = AdUnitIds(android = "…", ios = "…"),
cachePolicy = AdCachePolicy(
maxSize = 3,
reloadAfterShow = true,
expirationPolicy = AdExpirationPolicy(
fullScreenTtl = 1.hours,
appOpenTtl = 4.hours,
nativeTtl = 1.hours,
),
),
)
Full-screen ad lifecycle: load, cache, show, reloadThe FullScreenSlotCore state machine, its TTL'd FIFO cache, the generation counter bumped by clear(), and why show() is not reentrant per controller.IdleLoadingAdRetry backoffLoadedadmitted to the cachePresentingSDK owns the screenload()successshow()FIFO · maxSize (default 1) · TTL 1h, app-open 4hload failedFailedload() again to retrypresentAd suspendsuntil dismissalDismissedSDK terminal callbackreloadAfterShow?yes → scheduleReloadnoIdle if the cache is empty,Loaded if it is notclear() and the generation counterclear() bumps slotState.generation andretires the cached ads.A load or scheduled reload carries thegeneration it started with and re-checksit before publishing, so it can neverrepopulate a cache the caller just emptied.Presentation ownershippresentAd suspends until dismissal. Apresented ad is destroyed on normal returnonly — never on cancellation.FullScreenPresentationHandle is one-shot:after tryHandOffToCallbacks() the SDK ownsthe presentation and it is never force-closed.show() is not reentrant per controller — asecond call returns NotReady, not a queue.Every suspending operation is bounded: withTimeoutOrNull(loadTimeout) wraps the whole attempt sequence, retry backoff included.CLAUDE.md invariants #1, #2 and #9.
Full-screen ad lifecycle: load, cache, show, reloadCLAUDE.md invariants #1, #2, #9Read this diagram in words

The mechanics the diagram shows: the cache is a per-slot FIFO deque up to maxSize. load() fills it sequentially and a partial fill still reports Loaded. show() consumes the oldest entry first. Eviction happens on every touch. And clear() bumps a generation counter so any load or scheduled reload still in flight for the old generation is invalidated instead of repopulating a cache the caller just emptied.

The constraint checks the constructors enforce, because they throw rather than clamp: AdCachePolicy.maxSize must be at least 1, and every duration in AdExpirationPolicy must be finite and positive. The cache also evicts on read, so a load() that finds expired entries drops them before reporting Loaded with the remaining valid count.

A practical note on load() returning a partial fill. A partial fill happens when the platform SDK returns fewer ads than requested before the call resolves — usually because the network returned a smaller batch than the publisher asked for. The library still reports Loaded with whatever is in the cache, because the caller’s intent was “fill the cache”, and the cache is partially full. A call that returns zero ads is Failed with a no-fill error, not Loaded with an empty cache.

A native-pool cross-reference in one sentence: for native placements the same maxSize budgets available + in-use ads together — see the native ads guide.

AdPlacement(
id = "interstitial_level_end",
format = AdFormat.Interstitial,
adUnitIds = AdUnitIds(android = "…", ios = "…"),
retryPolicy = AdRetryPolicy(
maxAttempts = 2, // total attempts, including the first
initialDelay = 2.seconds,
maxDelay = 30.seconds,
backoffMultiplier = 2.0,
),
)
Retry timeline: capped exponential backoffAdRetryPolicy's default of two attempts, how each backoff delay is computed and capped, which GMA failures are retried, and the timeout that bounds the whole sequence.AdRetryPolicy defaults — maxAttempts 2, initialDelay 2s, backoffMultiplier 2.0, maxDelay 30smaxAttempts = 2 (default): initial attempt + one retryattempt 1retry 1maxAttempts = 4 (configured)attempt 1retry 1retry 2retry 30s2s6s14s+2s+4s+8sthen capped at maxDelay = 30swithTimeoutOrNull(loadTimeout) bounds the whole sequence, backoff includedRetried — transient failuresAndroid (enum names): NETWORK_ERROR,TIMEOUT, INTERNAL_ERRORiOS (numeric codes): 2 network, 5 timeout,11 internaldelay(n) = min(initialDelay × 2^(n−1), maxDelay)Never retriedNO_FILL (Android) / code 1 (iOS) — noinventory; retrying burns requests anddepresses fill rate.INVALID_REQUEST, APP_ID_MISSING,INVALID_AD_RESPONSE — configuration.consent_required, sdk_not_ready — gates,not load failures.maxAttempts counts the initial attempt, so the default of 2 means one initial attempt plus one retry — CLAUDE.md invariant #9.
Retry timeline: capped exponential backoffCLAUDE.md invariants #9Read this diagram in words

maxAttempts counts the initial attempt plus retries, so the default of 2 means one retry and maxAttempts = 1 means none. Delay grows by backoffMultiplier after each attempt and is capped at maxDelay. The constructor requires maxAttempts >= 1, finite positive delays, maxDelay >= initialDelay, and backoffMultiplier >= 1.0.

The classification, which is the part that surprises people:

FailureRetried?
Network, timeout, internal error — GMA code 0/2 on Android, 2/5/11 on iOSYes, per AdRetryPolicy
No fill — GMA code 3 on Android, 1 on iOSNo. This is normal; retry later at a natural moment
AdErrorCode.CONSENT_REQUIREDNo. Resolve consent first
AdErrorCode.SDK_NOT_READYNo. Wait for AdManagerStatus.Ready

A “no” here means the library does not schedule a retry. The publisher can still call load() again, of course — the policy is about automatic retries, not caller-initiated ones.

A note on maxAttempts = 1. Setting maxAttempts to 1 disables the retry layer entirely — the call returns whatever the platform SDK returns, including transient failures. The right shape for that policy is a caller that has its own retry mechanism (a button-driven retry, a backoff at a higher level). The wrong shape is “I want to load faster” — maxAttempts = 1 is not faster than maxAttempts = 2 unless the network is consistently failing on the first try, in which case the underlying problem needs investigation, not a policy tweak.

What stops a load or a presentation hanging forever?

Section titled “What stops a load or a presentation hanging forever?”
AdPlacement(
id = "interstitial_level_end",
format = AdFormat.Interstitial,
adUnitIds = AdUnitIds(android = "…", ios = "…"),
timeoutPolicy = AdTimeoutPolicy(
loadTimeout = 30.seconds,
presentationHandOffTimeout = 10.seconds,
),
)

Why this policy exists at all, because it is the least obvious of the four: GMA is a listener SDK, so if a terminal callback never arrives an unbounded load() or show() suspends forever. For show() that is worse than a hang — the presentation token is process-wide, so one wedged presentation blocks every full-screen ad in the app until the process dies.

The precise semantics: loadTimeout is the maximum time for one load including retry backoff; on expiry the state becomes AdLoadState.Failed and a late ad is rejected and destroyed by the generation check, exactly as after a clear(). presentationHandOffTimeout bounds the time between committing an ad to presentation and the SDK reporting anything — it does not bound how long a user watches an ad.

A note on why presentationHandOffTimeout exists. The platform SDKs present ads through a delegate-based pattern: the library hands the ad to the SDK and the SDK calls back when the presentation is complete. If the SDK never calls back — a real failure mode on iOS when the app is backgrounded mid-hand-off — the presentation token never releases, and the process-wide ad presence counter is permanently off-by-one. The timeout is the safety valve that releases the token after a bounded wait even if the SDK never reports.

Concrete recommendations rather than platitudes:

  • Interstitials at a natural break: maxSize = 1, reloadAfterShow = true. One ad, always warm.
  • Rewarded ads behind a user-initiated button: maxSize = 1 and preload when the screen opens, so the button is never dead.
  • Bursty full-screen moments, such as end-of-level in a game: maxSize = 2 or 3, but only if all of them can plausibly be shown within the one-hour TTL.
  • Native feeds: size to the number of ad slots that can be on screen at once, plus one.
  • Raise loadTimeout only for genuinely slow networks; lowering it below about 10 seconds mostly converts fill into failures.

The native ads guide has the maxSize-for-native sizing in detail, because the same value budgets available plus in-use and the calculation is different.

A note on “always warm”. The recommendation reloadAfterShow = true makes the cache refill in the background after each show, so the next break has an ad ready. The cost is the same as a regular load() — the publisher pays the network round-trip for a request that may or may not be shown. For a placement that fires ten times a day, that is ten extra requests; for a placement that fires twice a day, that is two. The math favours reloadAfterShow = true whenever the placement is in a hot path, and false when the placement is shown at a moment the user is unlikely to reach twice.