Optimizing Ad Refresh Rates: GAM Inventory Declarations, OpenRTB Refresh Objects, and Page Visibility API

Executing googletag.pubads().refresh() on an unthrottled 30-second setInterval loop without active viewability gating or upstream SSP signaling destroys

Minimalist 3D glass chronograph ring with thirty emerald segments and a central sapphire lens casting a cyan beam on an active ad card while a shadowed card rests in an amber paused state.

Executing googletag.pubads().refresh() on an unthrottled 30-second setInterval loop without active viewability gating or upstream SSP signaling destroys slot-level eCPMs within two bidding cycles. When an ad slot refreshes while out of the browser viewport or while the tab is backgrounded, the rendered creative fails to register an Active View or Media Rating Council (MRC) viewable impression (defined as at least 50% of pixels in view for 1 continuous second for display, or 2 continuous seconds for video). Tier-1 DSPs track historical viewability completion rates and attention durations at the placement, domain, and SupplyChain (schain) node level. Once a slot's aggregate viewability drops below buyer campaign thresholds (typically 60% to 70%), bid shading algorithms aggressively discount clearing prices or QPS throttling rules drop the placement from pre-bid targeting entirely.

Recovering and compounding session yield requires engineering ad refresh as a coordinated state machine across three distinct layers of the programmatic stack. First, publishers must classify and declare refresh mechanics inside Google Ad Manager (GAM) Inventory Rules so AdX and Open Bidding buyers receive accurate pre-bid metadata. Second, Prebid.js and server-side OpenRTB 2.6 adapters must populate the standardized Refresh and RefSettings objects so participating SSP partners can price each subsequent impression accurately. Third, the client-side wrapper must replace naive wall-clock intervals with an intersection-aware, focus-aware chronograph built on the W3C IntersectionObserver and Page Visibility APIs.

At Sky Boosts, our monetization platform enforces strict, viewability-verified refresh orchestration across web, mobile app, and CTV supply. By pairing transparent upstream bid request declarations with deterministic client-side timer management, publishers expand impression volume per session without triggering DSP bid shading or Google policy enforcement.

Google Ad Manager enforces strict transparency policies governing any ad unit that reloads creatives within a single pageview or application session. Under Google's programmatic policies for AdX and Open Bidding, publishers are required to declare refreshing inventory in Inventory Rules (Inventory > Inventory rules > Publisher declarations) and specify both the trigger mechanism and the minimum interval between refreshes. Leaving refreshing slots undeclared is a common cause of policy warnings, clawbacks, and sudden AdX demand suspension when Google's automated crawlers detect repeated pubads().refresh() calls originating from identical page correlates (correlator / scor parameters) on the same ad unit path.

From an auction mechanics perspective, GAM declarations attach explicit refresh metadata to outgoing AdX and Open Bidding bid requests. Buyers rely on this metadata to distinguish between a primary page-load impression (count = 0) and subsequent refreshed impressions (count >= 1). When the declaration matches the actual runtime behavior observed by Google's GPT and GMA SDK telemetry, DSPs can bid with confidence on refreshed supply, pricing the inventory according to its verified exposure duration rather than applying a blanket penalty across the entire property.

User Action-Based (No Minimum) vs. Event-Triggered (30s Min) vs. Time-Interval (30s Web / 60s Rec App)

To configure compliant declarations, AdOps teams must map every refreshing slot to the exact rules outlined in the Google Ad Manager refreshing inventory policy documentation, which segments triggers into three mutually exclusive classifications:

  • User action-based refresh (No minimum interval): The ad slot refreshes strictly in direct response to an intentional user navigation or interaction event, such as clicking to the next slide in a photo gallery, filtering a product data table, or switching views inside a single-page application (SPA) where the content context materially changes. Because the user explicitly initiates the state change, Google imposes no minimum time interval between refreshes, though the new ad must remain aligned with standard placement policies.
  • Event-triggered refresh (30-second minimum on web and app; 60-second recommended on app): The refresh is driven by an automated publisher or application event rather than direct user navigation, such as a live sports scoreboard updating after a play, a financial ticker crossing a threshold, or a breaking news feed appending a new entry. Google mandates a hard minimum of 30 seconds between refreshes across both web and mobile apps, and explicitly recommends a 60-second minimum for mobile app inventory. Crucially, the policy specifies that repeating events occurring at intervals under 120 seconds do not qualify as event-triggered refreshes and must instead be declared as time-interval refreshes.
  • Time-interval refresh (30-second minimum on web and app; 60-second recommended on app): The ad slot reloads automatically at predetermined time intervals while the user remains on the page or screen. Publishers must declare the exact minimum floor interval (selected from buckets of 30, 60, 90, 120, 180, 240, or 360 seconds). Every time-based refresh implementation must strictly honor the declared floor; if a slot is declared at 60 seconds, firing a refresh at 58 seconds due to client-side timer drift violates the declaration and risks enforcement.

When monetizing standard banner and MREC formats on high-engagement editorial or utility pages, time-interval refresh is the dominant architecture. However, selecting the 30-second minimum bucket solely to maximize raw request volume frequently backfires in first-price auctions.

Why Some Buyers Filter Below 240-Second Refresh Declarations

While a 30-second or 60-second time-based refresh complies with Google Ad Manager policy minimums, buy-side bidding behavior introduces a secondary constraint. As noted directly in Google's declaration guidelines, certain buyers and DSP seat configurations filter out or refuse to purchase time-based refreshing inventory declared at intervals below 240 seconds.

This 240-second threshold stems from enterprise brand-safety, attention-metric, and video-completion requirements on the buy side. Agency trading desks optimizing toward cost-per-completed-view (CPCV), extended brand exposure, or post-view conversion attribution model short-interval refreshes (30s to 90s) as high-churn placements where the creative may be replaced before the user finishes processing the message or interacting with rich media elements. Furthermore, when outstream or instream video creatives (15s to 30s duration) compete in multi-format banner slots, a 30-second refresh cycle risks terminating the video player mid-playback if network latency or VAST wrapper resolution delays the initial render by a few seconds.

Consequently, yield engineers should segment refresh declarations by placement archetype rather than applying a uniform 30-second rule across the entire network:

  1. Sticky Footers, Adhesion Units, and Persistent Sidebars (30s to 60s Active In-View): These high-viewability slots capture immediate, continuous viewport presence. A 30-second to 60-second active in-view interval maximizes session revenue because performance and direct-response DSPs bid aggressively on high-CTR, high-viewability sticky units regardless of shorter refresh horizons.
  2. In-Content Editorial and B2B / Finance Placements (120s to 240s Active In-View): For premium upper-funnel placements attracting high-CPM PMP and guaranteed brand budgets, configuring a 120-second or 240-second declaration unlocks incremental demand from strict brand buyers that block sub-240s slots. Publishers can model the trade-off between impression multiplier and eCPM floor elasticity on their own traffic before committing changes to production GAM rules.

OpenRTB 2.6 Refresh and RefSettings Objects (v2.6-202303+)

Declaring refresh rules inside GAM informs AdX and Open Bidding, but header bidding demand routed through Prebid.js, Prebid Server, or custom server-to-server (S2S) connections requires an equivalent protocol-level signal. Historically, SSPs relied on proprietary imp.ext fields to pass refresh telemetry. That fragmentation ended when the IAB Tech Lab OpenRTB 2.6 specification (specifically from version v2.6-202303 onward) standardized the Refresh and RefSettings objects directly inside the Banner, Video, Audio, and Native impression sub-objects.

The OpenRTB Refresh object communicates two critical facts to the DSP for every auction: the structural rules governing how the slot refreshes (refsettings), and the ordinal number of refreshes already executed on that specific slot during the current page session (count). Inside RefSettings, the protocol defines three core attributes:

  • reftype (integer): Classifies the trigger mechanism using standardized enumerations: 0 for unknown, 1 for user-action-based refresh, 2 for event-triggered refresh, and 3 for time-based auto-refresh.
  • minint (integer): Specifies the minimum uninterrupted exposure time in seconds that the slot guarantees before the next refresh occurs.
  • ext (object): Placeholder for exchange-specific extensions (such as signaling whether the timer pauses when out of view).

Passing an accurate OpenRTB 2.6 payload allows DSPs to evaluate refresh supply without guessing. Below is a production-grade OpenRTB 2.6 bid request fragment illustrating a 300x250 banner slot on its second refresh (count: 2) using a 30-second time-based trigger (reftype: 3, minint: 30):

{
  "id": "80ce30c53c16e",
  "imp": [
    {
      "id": "1",
      "tagid": "/21800000000/editorial_sidebar_mrec_1",
      "bidfloor": 1.45,
      "bidfloorcur": "USD",
      "banner": {
        "w": 300,
        "h": 250,
        "pos": 1,
        "refresh": {
          "refsettings": [
            {
              "reftype": 3,
              "minint": 30
            }
          ],
          "count": 2
        }
      }
    }
  ],
  "site": {
    "domain": "publisher-example.com",
    "page": "https://publisher-example.com/market-analysis"
  },
  "tmax": 1000
}

In Prebid.js 11.x environments, wrapper engineers should configure ortb2Imp enrichment logic to increment imp.banner.refresh.count on each auction cycle for a given ad unit code while keeping refsettings synchronized with the corresponding GAM Inventory Rule declaration. When both GAM and OpenRTB 2.6 payloads declare identical minint and trigger classifications, supply path optimization (SPO) auditors verify the publisher as fully compliant across all demand channels.

Designing an MRC-Compliant In-View Timer with the Page Visibility API

Signaling a 30-second minimum exposure interval via GAM declarations and OpenRTB minint: 30 creates an engineering contract: the ad creative must genuinely accumulate 30 seconds of viewable, active screen exposure before the wrapper initiates the next auction. A basic setTimeout started on slotRenderEnded violates this contract whenever the user scrolls the ad out of the viewport or switches browser tabs.

Circular 30-second ad refresh chronograph showing green active viewability segments pausing with an amber indicator at segment 18 when the browser tab is hidden.

To fulfill the exposure contract deterministically, the client-side ad controller must accumulate time only when three conditions are simultaneously true:

  1. Creative Render Confirmation: GPT has fired slotRenderEnded with !event.isEmpty, confirming a creative is mounted in the DOM.
  2. MRC Viewport Intersection: An IntersectionObserver attached to the slot container reports intersectionRatio >= 0.50 (50% or more of the ad's pixels are inside the active viewport).
  3. Foreground Tab Visibility: The browser tab itself is visible to the user, verified via the W3C Page Visibility API.

Pausing Timers on Tab Hide (document.visibilityState === 'hidden')

Modern browsers throttle background tab execution, but IntersectionObserver retains its last calculated intersection state when a user switches tabs or minimizes the browser window. If a user is looking at an ad slot (intersectionRatio === 1.0) and presses Cmd+Tab or switches to another browser tab for 45 seconds, an intersection-only timer will falsely conclude that the ad was in view for the entire 45-second period and trigger an immediate refresh upon tab return (or while still backgrounded).

As detailed in the MDN Page Visibility API specification, browsers fire the visibilitychange event on the document object and update document.visibilityState to either 'visible' or 'hidden' whenever the user switches tabs, minimizes the window, or locks the device screen. By binding a listener to visibilitychange, the refresh controller can immediately halt elapsed time accumulation the millisecond document.visibilityState === 'hidden' evaluates to true, preserving the exact remaining millisecond balance until the user returns.

The following production-ready JavaScript module coordinates GPT, Prebid.js, IntersectionObserver, and the Page Visibility API to enforce a strict 30,000ms cumulative active in-view refresh cycle:

class ActiveViewRefreshController {
  constructor(slot, divId, pbAdUnitCode, minIntervalMs = 30000) {
    this.slot = slot;
    this.divId = divId;
    this.pbAdUnitCode = pbAdUnitCode;
    this.minIntervalMs = minIntervalMs;
    this.accumulatedMs = 0;
    this.lastTickTimestamp = null;
    this.isInViewport = false;
    this.isRendered = false;
    this.refreshCount = 0;
    this.timerId = null;

    this.initObservers();
  }

  initObservers() {
    const el = document.getElementById(this.divId);
    if (!el) return;

    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        this.isInViewport = entry.intersectionRatio >= 0.5;
        this.evaluateTimerState();
      });
    }, { threshold: [0, 0.5, 1.0] });
    observer.observe(el);

    document.addEventListener('visibilitychange', () => {
      this.evaluateTimerState();
    });

    window.googletag = window.googletag || { cmd: [] };
    googletag.cmd.push(() => {
      googletag.pubads().addEventListener('slotRenderEnded', (event) => {
        if (event.slot === this.slot) {
          this.isRendered = !event.isEmpty;
          this.accumulatedMs = 0;
          this.lastTickTimestamp = null;
          this.evaluateTimerState();
        }
      });
    });
  }

  isActivelyViewable() {
    return (
      this.isRendered &&
      this.isInViewport &&
      document.visibilityState === 'visible'
    );
  }

  evaluateTimerState() {
    if (this.isActivelyViewable()) {
      if (!this.timerId) {
        this.lastTickTimestamp = performance.now();
        const remainingMs = Math.max(0, this.minIntervalMs - this.accumulatedMs);
        this.timerId = setTimeout(() => this.executeRefresh(), remainingMs);
      }
    } else if (this.timerId) {
      clearTimeout(this.timerId);
      this.timerId = null;
      if (this.lastTickTimestamp !== null) {
        this.accumulatedMs += performance.now() - this.lastTickTimestamp;
        this.lastTickTimestamp = null;
      }
    }
  }

  executeRefresh() {
    this.timerId = null;
    this.isRendered = false;
    this.accumulatedMs = 0;
    this.refreshCount += 1;

    pbjs.que.push(() => {
      pbjs.requestBids({
        adUnitCodes: [this.pbAdUnitCode],
        ortb2Imp: {
          [this.pbAdUnitCode]: {
            banner: {
              refresh: {
                refsettings: [{ reftype: 3, minint: this.minIntervalMs / 1000 }],
                count: this.refreshCount
              }
            }
          }
        },
        bidsBackHandler: () => {
          pbjs.setTargetingForGPTAsync([this.pbAdUnitCode]);
          googletag.pubads().refresh([this.slot]);
        }
      });
    });
  }
}

Notice how evaluateTimerState() uses performance.now() deltas rather than fixed polling ticks. If a creative renders in view for 12,400ms before the user switches tabs (document.visibilityState === 'hidden'), clearTimeout cancels the pending refresh and banks 12400 into this.accumulatedMs. When the user returns to the tab 5 minutes later, the controller calculates remainingMs as 17600 (30000 - 12400) and schedules the completion timer for the exact remainder.

For AdOps teams auditing their existing wrapper stack, implementing this three-pillar architecture (aligned GAM Inventory Rules, OpenRTB 2.6 Refresh signaling, and visibilitychange-gated timers) immediately stabilizes slot viewability above 75% to 85%. Audit your GAM Active View % Viewable and Active View Average Viewable Time reports segmented by Refresh count weekly: if refreshed impressions (count >= 1) exhibit lower average viewable duration than initial load impressions (count = 0), your client-side timer is leaking ticks during backgrounded or out-of-viewport states and should be refactored to the state-machine pattern above.

Next step

Put it to work.

Every pattern in this article runs on our unified SSP and monetization platform: GAM and MCM management, Prebid client and server-side, and CTV SSAI.

Net-30 / Net-60 terms · 100% brand-safe demand