{"id":1368,"date":"2026-06-07T17:00:00","date_gmt":"2026-06-07T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1368"},"modified":"2026-03-07T03:35:10","modified_gmt":"2026-03-07T08:35:10","slug":"banner-frequency-capping","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/banner-frequency-capping\/","title":{"rendered":"Banner Frequency Capping: Finding the Right Balance"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Every time your smart banner appears, it has a diminishing chance of converting the user. The first impression has the highest conversion probability. By the tenth impression, the user has already decided not to install and the banner is just noise. Frequency capping limits how often the banner appears, protecting user experience while preserving the conversion opportunities that matter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers frequency capping strategies, implementation approaches, and how to find the right balance for your audience. For dismiss behavior (what happens when users actively close the banner), see <a href=\"https:\/\/tolinku.com\/blog\/banner-dismiss-behavior\/\">banner dismiss behavior<\/a>. For banner scheduling, see <a href=\"https:\/\/tolinku.com\/blog\/banner-scheduling-campaigns\/\">banner scheduling for campaigns<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><img decoding=\"async\" src=\"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/screenshot-banners-list-1772822961077.png\" alt=\"Tolinku smart banners list with status, segment, and scheduling info\">\n<em>The smart banners list showing banner titles, status toggles, segments, and action buttons.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why Frequency Cap<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Diminishing Returns<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Banner conversion rates follow a predictable decay curve:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Impression #<\/th>\n<th>Relative CTR<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>1<\/td>\n<td>100% (baseline)<\/td>\n<\/tr>\n<tr>\n<td>2<\/td>\n<td>70-80%<\/td>\n<\/tr>\n<tr>\n<td>3<\/td>\n<td>50-60%<\/td>\n<\/tr>\n<tr>\n<td>5<\/td>\n<td>30-40%<\/td>\n<\/tr>\n<tr>\n<td>10<\/td>\n<td>10-20%<\/td>\n<\/tr>\n<tr>\n<td>15+<\/td>\n<td>&lt; 5%<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">After 5-10 impressions, the marginal conversion value of each additional impression approaches zero, while the annoyance cost continues to accumulate.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">User Experience Cost<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Each unnecessary banner impression:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Takes up screen space that could show content.<\/li>\n<li>Adds cognitive load (the user must decide to ignore or dismiss it again).<\/li>\n<li>Erodes trust (&quot;this site keeps pushing its app on me&quot;).<\/li>\n<li>Can increase bounce rate if the banner is too persistent.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">The Balance<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The goal is to show the banner enough times to capture users who are genuinely interested, but stop before you annoy users who are not. The right cap depends on your app, your audience, and the banner&#39;s content.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Types of Frequency Caps<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Impression Cap<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Limit the total number of times the banner is shown to a user:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function trackImpression() {\n  const count = parseInt(localStorage.getItem(&#39;banner_impressions&#39;) || &#39;0&#39;) + 1;\n  localStorage.setItem(&#39;banner_impressions&#39;, count.toString());\n  return count;\n}\n\nfunction isWithinCap() {\n  const impressions = parseInt(localStorage.getItem(&#39;banner_impressions&#39;) || &#39;0&#39;);\n  return impressions &lt; 10; \/\/ Show up to 10 times total\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Recommended caps:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Evergreen banners<\/strong> (generic &quot;get the app&quot;): 5-10 impressions total<\/li>\n<li><strong>Campaign banners<\/strong> (time-limited promotions): 3-5 impressions<\/li>\n<li><strong>Aggressive banners<\/strong> (full-screen, floating): 2-3 impressions<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Time-Window Cap<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Limit impressions within a rolling time window:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function isWithinTimeWindowCap() {\n  const impressionLog = JSON.parse(localStorage.getItem(&#39;banner_impression_log&#39;) || &#39;[]&#39;);\n  const windowMs = 24 * 60 * 60 * 1000; \/\/ 24 hours\n  const maxPerWindow = 3;\n\n  const now = Date.now();\n  const recentImpressions = impressionLog.filter(ts =&gt; (now - ts) &lt; windowMs);\n\n  return recentImpressions.length &lt; maxPerWindow;\n}\n\nfunction logImpression() {\n  const impressionLog = JSON.parse(localStorage.getItem(&#39;banner_impression_log&#39;) || &#39;[]&#39;);\n  impressionLog.push(Date.now());\n\n  \/\/ Keep only recent entries to avoid localStorage bloat\n  const cutoff = Date.now() - (30 * 24 * 60 * 60 * 1000);\n  const trimmed = impressionLog.filter(ts =&gt; ts &gt; cutoff);\n  localStorage.setItem(&#39;banner_impression_log&#39;, JSON.stringify(trimmed));\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Recommended caps:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>1-2 impressions per session<\/li>\n<li>3-5 impressions per day<\/li>\n<li>10-15 impressions per week<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Session Cap<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Limit impressions per browsing session:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function isWithinSessionCap() {\n  const sessionImpressions = parseInt(sessionStorage.getItem(&#39;banner_session_impressions&#39;) || &#39;0&#39;);\n  return sessionImpressions &lt; 1; \/\/ Show once per session\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A session cap of 1 means the user sees the banner once when they visit. If they dismiss it, it does not reappear during that visit. This is the least intrusive approach.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Page-Type Cap<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Show the banner only on certain page types, limiting total exposure:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function shouldShowOnPage() {\n  const path = window.location.pathname;\n\n  \/\/ Show on product pages and article pages\n  if (path.startsWith(&#39;\/products\/&#39;) || path.startsWith(&#39;\/articles\/&#39;)) {\n    return true;\n  }\n\n  \/\/ Don&#39;t show on checkout, account, or utility pages\n  return false;\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is not a frequency cap per se, but it limits the pages where the banner appears, effectively capping how often the user encounters it during a typical browse.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Combined Frequency Strategy<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The most effective approach combines multiple cap types:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function shouldShowBanner() {\n  \/\/ 1. Has the user already installed the app?\n  if (hasInstalledApp()) return false;\n\n  \/\/ 2. Is the banner permanently dismissed?\n  if (isPermanentlyDismissed()) return false;\n\n  \/\/ 3. Is the dismiss timer still active?\n  if (isDismissTimerActive()) return false;\n\n  \/\/ 4. Session cap: max 1 per session\n  if (getSessionImpressions() &gt;= 1) return false;\n\n  \/\/ 5. Daily cap: max 3 per day\n  if (getDailyImpressions() &gt;= 3) return false;\n\n  \/\/ 6. Total cap: max 15 lifetime\n  if (getTotalImpressions() &gt;= 15) return false;\n\n  \/\/ 7. Page type check\n  if (!isHighValuePage()) return false;\n\n  return true;\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This ensures:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>No more than 1 impression per visit.<\/li>\n<li>No more than 3 per day.<\/li>\n<li>No more than 15 total.<\/li>\n<li>Only on pages where the banner is relevant.<\/li>\n<li>Respects active dismissals.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Resetting the Cap<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">New Campaign Reset<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When you launch a new campaign with a fresh banner, consider resetting the impression counter:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function checkForNewCampaign(currentCampaignId) {\n  const lastCampaignId = localStorage.getItem(&#39;banner_campaign_id&#39;);\n\n  if (lastCampaignId !== currentCampaignId) {\n    \/\/ New campaign: reset impression counters\n    localStorage.setItem(&#39;banner_campaign_id&#39;, currentCampaignId);\n    localStorage.setItem(&#39;banner_impressions&#39;, &#39;0&#39;);\n    localStorage.removeItem(&#39;banner_impression_log&#39;);\n    \/\/ Keep dismiss state (don&#39;t reset if user actively dismissed)\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Reset the impression counter, but do not reset the dismiss state. A user who actively dismissed the banner has made a stronger signal than one who simply saw it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">App Detection Reset<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you detect that the user has installed the app (via a cookie set by the app, or by checking Universal Links\/App Links), stop showing the banner entirely:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function hasInstalledApp() {\n  \/\/ Check for a cookie set by the app&#39;s web-to-app bridge\n  if (document.cookie.includes(&#39;app_installed=true&#39;)) return true;\n\n  \/\/ Check for a localStorage flag set by the app\n  if (localStorage.getItem(&#39;app_installed&#39;) === &#39;true&#39;) return true;\n\n  return false;\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Measuring Cap Effectiveness<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Metrics to Track<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Conversion rate by impression number:<\/strong> At which impression does the conversion rate approach zero? That is your ideal cap.<\/li>\n<li><strong>Banner CTR with different caps:<\/strong> Test cap of 5 vs. 10 vs. 15. Does the higher cap actually produce more installs, or just more impressions?<\/li>\n<li><strong>Bounce rate by cap:<\/strong> Does a higher cap increase bounce rate?<\/li>\n<li><strong>User satisfaction:<\/strong> If you run surveys, include a question about the app banner frequency.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Finding Your Optimal Cap<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Run an A\/B test with different cap settings:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function getCapVariant() {\n  const variants = [\n    { maxTotal: 5, maxDaily: 2, id: &#39;low&#39; },\n    { maxTotal: 10, maxDaily: 3, id: &#39;medium&#39; },\n    { maxTotal: 20, maxDaily: 5, id: &#39;high&#39; },\n  ];\n\n  const hash = simpleHash(getUserId());\n  return variants[hash % variants.length];\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Measure total installs per variant over 2-4 weeks. The variant with the highest installs per unique visitor (not per impression) is your optimal cap.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku Frequency Capping<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/smart-banners\">Tolinku&#39;s smart banners<\/a> include configurable frequency caps. Set impression limits, time windows, and session caps through the <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/smart-banners\/display-behavior\/\">dashboard<\/a> without writing code. The SDK tracks impressions automatically and respects all configured caps.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For dismiss behavior after cap is reached, see <a href=\"https:\/\/tolinku.com\/blog\/banner-dismiss-behavior\/\">banner dismiss behavior<\/a>. For campaign-based cap resets, see <a href=\"https:\/\/tolinku.com\/blog\/banner-scheduling-campaigns\/\">banner scheduling<\/a>. For the complete setup, see the <a href=\"https:\/\/tolinku.com\/blog\/smart-app-banners-complete-guide\/\">smart banners guide<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Set the right frequency caps for smart banners. Balance conversion opportunities with user experience using impression limits, time windows, and behavioral triggers.<\/p>\n","protected":false},"author":2,"featured_media":1367,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Banner Frequency Capping: Finding the Right Balance","rank_math_description":"Set the right frequency caps for smart banners. Balance conversion opportunities with user experience using impression limits and time windows.","rank_math_focus_keyword":"banner frequency capping","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-banner-frequency-capping.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-banner-frequency-capping.png","footnotes":""},"categories":[16],"tags":[37,254,39,110,69,40,33,41],"class_list":["post-1368","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-marketing","tag-analytics","tag-best-practices","tag-conversion","tag-marketing","tag-mobile-development","tag-smart-banners","tag-user-experience","tag-web-to-app"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1368","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=1368"}],"version-history":[{"count":3,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1368\/revisions"}],"predecessor-version":[{"id":2291,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1368\/revisions\/2291"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1367"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1368"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1368"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1368"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}