{"id":1752,"date":"2026-07-15T09:00:00","date_gmt":"2026-07-15T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1752"},"modified":"2026-03-07T03:50:07","modified_gmt":"2026-03-07T08:50:07","slug":"real-time-attribution","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/real-time-attribution\/","title":{"rendered":"Real-Time Attribution for Mobile Campaigns"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Real-time attribution gives you install and conversion data within seconds of an event occurring. Instead of waiting for daily or hourly batch reports, you see every install, every in-app event, and every conversion as it happens. This matters most during campaign launches, flash sales, and fraud monitoring where delayed data means delayed decisions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers real-time attribution architecture and use cases. For attribution dashboards, see <a href=\"https:\/\/tolinku.com\/blog\/attribution-dashboards\/\">building attribution dashboards<\/a>. For postback URLs, see <a href=\"https:\/\/tolinku.com\/blog\/postback-urls-attribution\/\">postback URLs for mobile attribution<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">When Real-Time Attribution Matters<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Campaign Launch Monitoring<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When you launch a new campaign, you need to know within minutes whether:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Installs are coming in at the expected rate.<\/li>\n<li>CPI is within the target range.<\/li>\n<li>The correct creative and targeting are being served.<\/li>\n<li>There are no technical issues (broken tracking links, SDK errors).<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Waiting for a daily report means burning budget on a broken campaign for 24 hours.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Fraud Detection<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Fraud patterns are easier to detect in real-time:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A sudden spike in installs from a single source (click flooding).<\/li>\n<li>Installs with abnormally short click-to-install times (click injection).<\/li>\n<li>Geographic anomalies (installs from unexpected countries).<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">By the time a daily batch report reveals fraud, the damage is done.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Flash Sales and Time-Limited Campaigns<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">An e-commerce flash sale runs for 4 hours. You need to know within those 4 hours which channels are driving conversions and which are not.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Budget Pacing<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If your daily budget is $5,000 and you have spent $3,000 by noon, real-time data tells you whether to slow down or increase the budget.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Architecture<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Event Streaming Pipeline<\/h3>\n\n\n\n<pre><code>App SDK \u2192 Event Collector \u2192 Message Queue \u2192 Stream Processor \u2192 Dashboard\/Alerts\n                                              \u2193\n                                        Postback Sender\n                                              \u2193\n                                        Ad Network APIs\n<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">Components<\/h4>\n\n\n\n<ol class=\"wp-block-list\">\n<li><p><strong>Event Collector.<\/strong> Receives events from the app SDK (installs, in-app events). Must handle high throughput with low latency.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Message Queue.<\/strong> Buffers events for processing. Apache Kafka, AWS Kinesis, or Google Pub\/Sub. Provides durability and ordering.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Stream Processor.<\/strong> Matches clicks to installs in real-time. Runs attribution logic (last-click, deduplication, fraud checks).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Dashboard.<\/strong> Displays live metrics. WebSocket or SSE connection for push updates.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Postback Sender.<\/strong> Sends attribution results to ad networks immediately after attribution is determined.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Implementation<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">\/\/ Event collector endpoint\napp.post(&#39;\/v1\/events&#39;, async (req, res) =&gt; {\n  const event = validateEvent(req.body);\n\n  \/\/ Publish to message queue immediately\n  await kafka.produce(&#39;attribution-events&#39;, {\n    key: event.deviceId,\n    value: JSON.stringify(event),\n    timestamp: Date.now()\n  });\n\n  res.status(202).send({ status: &#39;accepted&#39; });\n});\n\n\/\/ Stream processor (Kafka consumer)\nasync function processEvent(event: AttributionEvent) {\n  if (event.type === &#39;install&#39;) {\n    \/\/ Look up recent clicks for this device\n    const clicks = await clickStore.getRecentClicks(\n      event.deviceId,\n      { within: &#39;7d&#39; }\n    );\n\n    if (clicks.length === 0) {\n      \/\/ Organic install\n      await attributeOrganic(event);\n    } else {\n      \/\/ Find the best matching click (last-click model)\n      const bestClick = clicks.sort((a, b) =&gt; b.timestamp - a.timestamp)[0];\n\n      \/\/ Run fraud checks\n      const fraudResult = checkFraud(event, bestClick);\n      if (fraudResult.isFraud) {\n        await rejectInstall(event, bestClick, fraudResult.reason);\n      } else {\n        await attributeInstall(event, bestClick);\n      }\n    }\n\n    \/\/ Send postback to ad network\n    await sendPostback(event, bestClick);\n\n    \/\/ Update real-time dashboard\n    await dashboardStream.publish(&#39;install&#39;, {\n      campaign: bestClick?.campaignId,\n      network: bestClick?.networkId,\n      country: event.country,\n      timestamp: event.timestamp\n    });\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Real-Time Click Store<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Attribution requires matching installs to clicks. For real-time attribution, the click store must be fast:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">\/\/ Redis-based click store for sub-millisecond lookups\nclass ClickStore {\n  async recordClick(click: Click): Promise&lt;void&gt; {\n    const key = `clicks:${click.deviceId}`;\n\n    await redis.zadd(key, click.timestamp, JSON.stringify(click));\n\n    \/\/ Expire clicks after the attribution window (7 days)\n    await redis.expire(key, 7 * 24 * 3600);\n  }\n\n  async getRecentClicks(deviceId: string, options: { within: string }): Promise&lt;Click[]&gt; {\n    const key = `clicks:${deviceId}`;\n    const windowMs = parseDuration(options.within);\n    const minTimestamp = Date.now() - windowMs;\n\n    const results = await redis.zrangebyscore(key, minTimestamp, &#39;+inf&#39;);\n    return results.map(r =&gt; JSON.parse(r));\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Real-Time Dashboards<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">WebSocket Updates<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Push attribution events to the dashboard in real-time:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">\/\/ Server: Push events via WebSocket\nwss.on(&#39;connection&#39;, (ws) =&gt; {\n  const subscription = dashboardStream.subscribe(&#39;install&#39;, (data) =&gt; {\n    ws.send(JSON.stringify({\n      type: &#39;install&#39;,\n      data: {\n        campaign: data.campaign,\n        network: data.network,\n        country: data.country,\n        timestamp: data.timestamp\n      }\n    }));\n  });\n\n  ws.on(&#39;close&#39;, () =&gt; subscription.unsubscribe());\n});\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Live Counters<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Display rolling counters for key metrics:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">\/\/ Rolling window counters (last 1 hour, last 24 hours)\nclass RollingCounter {\n  async increment(metric: string, dimensions: Record&lt;string, string&gt;): Promise&lt;void&gt; {\n    const key1h = `counter:1h:${metric}:${this.dimensionKey(dimensions)}`;\n    const key24h = `counter:24h:${metric}:${this.dimensionKey(dimensions)}`;\n\n    await redis.incr(key1h);\n    await redis.expire(key1h, 3600);\n\n    await redis.incr(key24h);\n    await redis.expire(key24h, 86400);\n  }\n\n  async get(metric: string, window: &#39;1h&#39; | &#39;24h&#39;, dimensions: Record&lt;string, string&gt;): Promise&lt;number&gt; {\n    const key = `counter:${window}:${metric}:${this.dimensionKey(dimensions)}`;\n    return parseInt(await redis.get(key) || &#39;0&#39;);\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Real-Time Alerting<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Set up alerts for anomalies:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">interface AlertRule {\n  metric: string;\n  condition: &#39;above&#39; | &#39;below&#39; | &#39;change&#39;;\n  threshold: number;\n  window: string;\n  channels: (&#39;slack&#39; | &#39;email&#39; | &#39;pagerduty&#39;)[];\n}\n\nconst alertRules: AlertRule[] = [\n  {\n    \/\/ CPI spike\n    metric: &#39;cpi&#39;,\n    condition: &#39;above&#39;,\n    threshold: 10.00,\n    window: &#39;1h&#39;,\n    channels: [&#39;slack&#39;]\n  },\n  {\n    \/\/ Install volume drop\n    metric: &#39;installs&#39;,\n    condition: &#39;below&#39;,\n    threshold: 50, \/\/ Less than 50 installs in 1 hour\n    window: &#39;1h&#39;,\n    channels: [&#39;slack&#39;, &#39;email&#39;]\n  },\n  {\n    \/\/ Fraud spike\n    metric: &#39;rejected_installs_rate&#39;,\n    condition: &#39;above&#39;,\n    threshold: 0.20, \/\/ More than 20% rejection rate\n    window: &#39;1h&#39;,\n    channels: [&#39;slack&#39;, &#39;pagerduty&#39;]\n  }\n];\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Real-Time vs Batch Trade-Offs<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Aspect<\/th>\n<th>Real-Time<\/th>\n<th>Batch<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Latency<\/td>\n<td>Seconds<\/td>\n<td>Hours<\/td>\n<\/tr>\n<tr>\n<td>Accuracy<\/td>\n<td>May include duplicates, late-arriving data can change results<\/td>\n<td>Final, reconciled<\/td>\n<\/tr>\n<tr>\n<td>Cost<\/td>\n<td>Higher (streaming infrastructure)<\/td>\n<td>Lower (scheduled queries)<\/td>\n<\/tr>\n<tr>\n<td>Complexity<\/td>\n<td>Higher (stream processing, state management)<\/td>\n<td>Lower (SQL queries)<\/td>\n<\/tr>\n<tr>\n<td>Use case<\/td>\n<td>Monitoring, fraud, launches<\/td>\n<td>Reporting, analysis, billing<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Most teams need both: real-time for monitoring and batch for reporting. The batch system serves as the source of truth; the real-time system is an early warning layer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Privacy Considerations<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Real-time attribution must comply with the same privacy regulations as batch attribution:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><a href=\"https:\/\/developer.apple.com\/documentation\/apptrackingtransparency\" rel=\"nofollow noopener\" target=\"_blank\">ATT<\/a>:<\/strong> Real-time attribution on iOS is limited by ATT opt-in rates. SKAdNetwork postbacks arrive with a delay (24-48 hours), so true real-time attribution is only possible for ATT-consented users.<\/li>\n<li><strong><a href=\"https:\/\/gdpr.eu\/\" rel=\"nofollow noopener\" target=\"_blank\">GDPR<\/a>:<\/strong> Real-time event streams that contain user identifiers must be processed in compliance with GDPR. Data minimization applies.<\/li>\n<li><strong>Data retention:<\/strong> Real-time data stored in Redis or streaming logs must follow your data retention policies.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku for Real-Time Analytics<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/analytics\">Tolinku&#39;s analytics<\/a> provide near-real-time deep link click and conversion tracking. View click events as they happen in the <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/analytics\/\">Tolinku dashboard<\/a>. For event-driven workflows, configure <a href=\"https:\/\/tolinku.com\/features\/webhooks\">webhooks<\/a> to receive real-time notifications when deep link events occur.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For attribution dashboards, see <a href=\"https:\/\/tolinku.com\/blog\/attribution-dashboards\/\">building attribution dashboards<\/a>. For mobile attribution, see <a href=\"https:\/\/tolinku.com\/blog\/mobile-attribution-developers-guide\/\">mobile attribution: a developer&#39;s guide<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Set up real-time attribution for instant campaign insights. Monitor installs, events, and conversions as they happen with live data feeds.<\/p>\n","protected":false},"author":2,"featured_media":1751,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Real-Time Attribution for Mobile Campaigns","rank_math_description":"Set up real-time attribution for instant campaign insights. Monitor installs, events, and conversions as they happen.","rank_math_focus_keyword":"real-time attribution","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-real-time-attribution.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-real-time-attribution.png","footnotes":""},"categories":[14],"tags":[37,28,520,20,69,274,271,294],"class_list":["post-1752","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-analytics","tag-analytics","tag-attribution","tag-campaign-measurement","tag-deep-linking","tag-mobile-development","tag-monitoring","tag-real-time","tag-streaming"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1752","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=1752"}],"version-history":[{"count":3,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1752\/revisions"}],"predecessor-version":[{"id":2698,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1752\/revisions\/2698"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1751"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1752"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1752"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1752"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}