{"id":1165,"date":"2026-05-23T13:00:00","date_gmt":"2026-05-23T18:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1165"},"modified":"2026-03-07T03:34:59","modified_gmt":"2026-03-07T08:34:59","slug":"real-time-event-processing","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/real-time-event-processing\/","title":{"rendered":"Real-Time Event Processing for Deep Link Data"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Batch processing is fine for dashboards you check once a day. It&#39;s not fine when you need to detect a broken campaign link within minutes, trigger a re-engagement email within seconds of an install, or shut down a referral fraud pattern before it costs you money.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Real-time event processing takes the push-based nature of <a href=\"https:\/\/tolinku.com\/features\/webhooks\">Tolinku webhooks<\/a> and extends it into a full event-driven architecture. Instead of storing events and querying them later, you process each event as it arrives: transforming, enriching, routing, and acting on it immediately. For the webhook setup basics, see the <a href=\"https:\/\/tolinku.com\/blog\/webhook-setup-guide\/\">webhook setup guide<\/a>. For streaming to analytics tools specifically, see the <a href=\"https:\/\/tolinku.com\/blog\/webhooks-analytics-pipelines\/\">analytics pipelines guide<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><img decoding=\"async\" src=\"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/platform-webhooks.png\" alt=\"Tolinku webhook configuration for event notifications\">\n<em>The webhooks page with create form, webhook list, and delivery log.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why Real-Time?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Batch processing (hourly or daily) works for reporting. Real-time processing enables:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Instant campaign feedback<\/strong>: See conversion rates change as you adjust targeting<\/li>\n<li><strong>Fraud detection<\/strong>: Catch anomalous referral patterns within minutes instead of discovering them in a monthly report<\/li>\n<li><strong>Triggered workflows<\/strong>: Send a push notification 5 minutes after an install, while the user&#39;s interest is still high<\/li>\n<li><strong>Dynamic routing<\/strong>: Adjust deep link destinations based on real-time traffic patterns<\/li>\n<li><strong>Live dashboards<\/strong>: Show stakeholders what&#39;s happening right now, not what happened yesterday<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Architecture Patterns<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Pattern 1: Direct Processing<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The simplest pattern. Your webhook receiver processes each event inline.<\/p>\n\n\n\n<pre><code>Tolinku \u2192 Receiver \u2192 Process \u2192 Store\/Act\n<\/code><\/pre>\n\n\n\n<pre><code class=\"language-typescript\">app.post(&#39;\/webhooks\/tolinku&#39;, async (req, res) =&gt; {\n  \/\/ Verify signature...\n  res.status(200).send(&#39;OK&#39;);\n\n  const event = JSON.parse(req.body.toString());\n\n  \/\/ Process inline\n  await enrichEvent(event);\n  await routeToDestinations(event);\n  await checkAlertConditions(event);\n});\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pros<\/strong>: Simple, no infrastructure beyond the receiver.\n<strong>Cons<\/strong>: Processing time adds up. If <code>enrichEvent<\/code> takes 200ms and <code>routeToDestinations<\/code> takes 300ms, you&#39;re spending 500ms per event. At 100 events\/second, you need concurrency or you&#39;ll fall behind.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for<\/strong>: Low-volume integrations (under 10 events\/second) with fast processing logic.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Pattern 2: Queue-Based Fan-Out<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Decouple ingestion from processing using a message queue.<\/p>\n\n\n\n<pre><code>Tolinku \u2192 Receiver \u2192 Queue \u2192 Worker 1 (analytics)\n                            \u2192 Worker 2 (alerts)\n                            \u2192 Worker 3 (CRM)\n<\/code><\/pre>\n\n\n\n<pre><code class=\"language-typescript\">import { SQSClient, SendMessageCommand } from &#39;@aws-sdk\/client-sqs&#39;;\n\nconst sqs = new SQSClient({});\n\napp.post(&#39;\/webhooks\/tolinku&#39;, async (req, res) =&gt; {\n  \/\/ Verify signature...\n  res.status(200).send(&#39;OK&#39;);\n\n  await sqs.send(new SendMessageCommand({\n    QueueUrl: process.env.SQS_QUEUE_URL!,\n    MessageBody: req.body.toString(),\n    MessageAttributes: {\n      EventType: {\n        DataType: &#39;String&#39;,\n        StringValue: req.headers[&#39;x-webhook-event&#39;] as string,\n      },\n    },\n  }));\n});\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each worker pulls from the queue (or a fan-out topic) and handles its own concern. Workers are independent: if the CRM integration is slow, it doesn&#39;t affect analytics ingestion.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for<\/strong>: Medium-volume integrations with multiple destinations or processing steps.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Pattern 3: Stream Processing<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For high-volume, low-latency requirements, use a streaming platform like Kafka or Amazon Kinesis.<\/p>\n\n\n\n<pre><code>Tolinku \u2192 Receiver \u2192 Kafka Topic \u2192 Stream Processor \u2192 Destinations\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The stream processor can:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Window events (count clicks per campaign per minute)<\/li>\n<li>Join event streams (correlate clicks with installs)<\/li>\n<li>Detect patterns (referral fraud, broken links)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for<\/strong>: High-volume integrations (100+ events\/second) where you need windowed aggregations or stream joins.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Event Enrichment<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Raw webhook events are useful but limited. Enrichment adds context that makes events actionable.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Geo-Enrichment<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>link.clicked<\/code> event includes an IP address. Resolve it to a country, region, and city:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">import { Reader } from &#39;@maxmind\/geoip2-node&#39;;\n\nlet geoReader: Reader;\n\nasync function initGeo() {\n  geoReader = await Reader.open(&#39;\/path\/to\/GeoLite2-City.mmdb&#39;);\n}\n\nfunction enrichWithGeo(event: any): any {\n  if (!event.data.ip) return event;\n\n  try {\n    const geo = geoReader.city(event.data.ip);\n    return {\n      ...event,\n      data: {\n        ...event.data,\n        country: geo.country?.isoCode || null,\n        region: geo.subdivisions?.[0]?.isoCode || null,\n        city: geo.city?.names?.en || null,\n      },\n    };\n  } catch {\n    return event;\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/dev.maxmind.com\/geoip\/geolite2-free-geolocation-data\" rel=\"nofollow noopener\" target=\"_blank\">MaxMind&#39;s GeoLite2<\/a> database is free and updates weekly. Tolinku uses the same database for its built-in analytics.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Campaign Metadata<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The webhook carries a <code>campaign<\/code> string. Your campaign management system has richer metadata: budget, target audience, creative variant, channel. Join on the campaign ID:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">async function enrichWithCampaign(event: any): Promise&lt;any&gt; {\n  if (!event.data.campaign) return event;\n\n  const campaign = await db.query(\n    &#39;SELECT channel, budget, target_audience FROM campaigns WHERE campaign_id = $1&#39;,\n    [event.data.campaign]\n  );\n\n  if (campaign.rows.length === 0) return event;\n\n  return {\n    ...event,\n    data: {\n      ...event.data,\n      channel: campaign.rows[0].channel,\n      budget: campaign.rows[0].budget,\n      target_audience: campaign.rows[0].target_audience,\n    },\n  };\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">User Identity<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For referral events, resolve the referral token to a user profile:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">async function enrichWithUser(event: any): Promise&lt;any&gt; {\n  if (!event.data.referrer_token) return event;\n\n  const user = await db.query(\n    &#39;SELECT id, email, name FROM users WHERE referral_token = $1&#39;,\n    [event.data.referrer_token]\n  );\n\n  if (user.rows.length === 0) return event;\n\n  return {\n    ...event,\n    data: {\n      ...event.data,\n      referrer_id: user.rows[0].id,\n      referrer_name: user.rows[0].name,\n    },\n  };\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Event Routing<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">After enrichment, route events to different destinations based on type and content.<\/p>\n\n\n\n<pre><code class=\"language-typescript\">interface EventRouter {\n  condition: (event: any) =&gt; boolean;\n  handler: (event: any) =&gt; Promise&lt;void&gt;;\n}\n\nconst routes: EventRouter[] = [\n  \/\/ All events go to the data warehouse\n  {\n    condition: () =&gt; true,\n    handler: sendToWarehouse,\n  },\n  \/\/ Install events trigger a welcome workflow\n  {\n    condition: (e) =&gt; e.event === &#39;install.tracked&#39;,\n    handler: triggerWelcomeWorkflow,\n  },\n  \/\/ Referral completions update the CRM\n  {\n    condition: (e) =&gt; e.event === &#39;referral.completed&#39;,\n    handler: updateCRM,\n  },\n  \/\/ High-value events go to Slack\n  {\n    condition: (e) =&gt; [&#39;install.tracked&#39;, &#39;referral.completed&#39;].includes(e.event),\n    handler: notifySlack,\n  },\n  \/\/ Anomalous click patterns trigger alerts\n  {\n    condition: (e) =&gt; e.event === &#39;link.clicked&#39; &amp;&amp; e.data.country === &#39;XX&#39;,\n    handler: flagSuspiciousClick,\n  },\n];\n\nasync function routeEvent(event: any) {\n  const enriched = await enrichEvent(event);\n\n  const promises = routes\n    .filter(route =&gt; route.condition(enriched))\n    .map(route =&gt; route.handler(enriched).catch(err =&gt;\n      console.error(`Route handler failed: ${err.message}`)\n    ));\n\n  await Promise.allSettled(promises);\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each route runs independently. A Slack notification failure doesn&#39;t prevent the warehouse write.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Windowed Aggregations<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Some insights require looking at events in aggregate, not individually. Windowed processing groups events by time and computes metrics over each window.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Sliding Window: Clicks Per Campaign Per Minute<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">interface WindowBucket {\n  campaign: string;\n  count: number;\n  firstSeen: number;\n}\n\nconst windows = new Map&lt;string, WindowBucket&gt;();\nconst WINDOW_SIZE_MS = 60_000; \/\/ 1 minute\n\nfunction recordClick(event: any) {\n  const campaign = event.data.campaign || &#39;unknown&#39;;\n  const key = `${campaign}`;\n\n  const bucket = windows.get(key);\n  if (bucket &amp;&amp; Date.now() - bucket.firstSeen &lt; WINDOW_SIZE_MS) {\n    bucket.count++;\n  } else {\n    \/\/ Emit the completed window\n    if (bucket) emitWindow(key, bucket);\n    windows.set(key, { campaign, count: 1, firstSeen: Date.now() });\n  }\n}\n\nfunction emitWindow(key: string, bucket: WindowBucket) {\n  console.log(`Campaign ${bucket.campaign}: ${bucket.count} clicks in window`);\n  \/\/ Send to dashboard, check thresholds, etc.\n}\n\n\/\/ Flush windows periodically\nsetInterval(() =&gt; {\n  const now = Date.now();\n  for (const [key, bucket] of windows.entries()) {\n    if (now - bucket.firstSeen &gt;= WINDOW_SIZE_MS) {\n      emitWindow(key, bucket);\n      windows.delete(key);\n    }\n  }\n}, 10_000);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For production workloads, use a proper stream processing framework (Kafka Streams, Apache Flink, or AWS Kinesis Data Analytics) instead of in-memory windows. But for moderate volumes, the in-memory approach works and avoids infrastructure complexity.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Exactly-Once Processing<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Webhooks are delivered at-least-once. Tolinku retries failed deliveries (3 retries at 1 minute, 5 minutes, and 30 minutes). Your processing pipeline must handle duplicates.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For event storage (warehouse, database), deduplicate on insert using a hash of the event content:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">const eventHash = crypto\n  .createHash(&#39;sha256&#39;)\n  .update(JSON.stringify(event))\n  .digest(&#39;hex&#39;);\n\nawait db.query(\n  &#39;INSERT INTO events (hash, event_type, data) VALUES ($1, $2, $3) ON CONFLICT (hash) DO NOTHING&#39;,\n  [eventHash, event.event, JSON.stringify(event)]\n);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For side effects (emails, Slack notifications, CRM updates), check the dedup table before acting:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">async function processOnce(event: any, handler: () =&gt; Promise&lt;void&gt;) {\n  const hash = crypto\n    .createHash(&#39;sha256&#39;)\n    .update(JSON.stringify(event))\n    .digest(&#39;hex&#39;);\n\n  const exists = await db.query(\n    &#39;SELECT 1 FROM processed_events WHERE hash = $1&#39;,\n    [hash]\n  );\n\n  if (exists.rows.length &gt; 0) return;\n\n  await handler();\n  await db.query(&#39;INSERT INTO processed_events (hash) VALUES ($1)&#39;, [hash]);\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">See the <a href=\"https:\/\/tolinku.com\/blog\/webhook-idempotency\/\">webhook idempotency guide<\/a> for a deeper treatment.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Scaling Considerations<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Volume<\/th>\n<th>Receiver<\/th>\n<th>Processing<\/th>\n<th>Storage<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>&lt; 10\/sec<\/td>\n<td>Single process<\/td>\n<td>Inline<\/td>\n<td>Direct DB insert<\/td>\n<\/tr>\n<tr>\n<td>10-100\/sec<\/td>\n<td>Load balanced<\/td>\n<td>Queue + workers<\/td>\n<td>Batch inserts<\/td>\n<\/tr>\n<tr>\n<td>100-1000\/sec<\/td>\n<td>Auto-scaling<\/td>\n<td>Kafka + stream processors<\/td>\n<td>Streaming warehouse load<\/td>\n<\/tr>\n<tr>\n<td>&gt; 1000\/sec<\/td>\n<td>Edge workers<\/td>\n<td>Kafka + Flink\/Spark<\/td>\n<td>Dedicated analytics cluster<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Most deep link integrations fall in the first two tiers. If you&#39;re processing over 100 events per second, you likely have a mature engineering team and existing streaming infrastructure to plug into.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Error Handling<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In a real-time pipeline, errors need to be handled without losing events:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Dead-letter queue (DLQ)<\/strong>: Events that fail processing go to a separate queue for investigation. Don&#39;t discard them.<\/li>\n<li><strong>Circuit breaker<\/strong>: If a downstream service (CRM, analytics API) is consistently failing, stop sending events to it temporarily. Queue them and replay when the service recovers.<\/li>\n<li><strong>Backpressure<\/strong>: If your processor can&#39;t keep up with the event rate, signal the queue to slow down rather than dropping events.<\/li>\n<\/ol>\n\n\n\n<pre><code class=\"language-typescript\">\/\/ Simple circuit breaker\nclass CircuitBreaker {\n  private failures = 0;\n  private lastFailure = 0;\n  private readonly threshold = 5;\n  private readonly resetMs = 30_000;\n\n  isOpen(): boolean {\n    if (this.failures &gt;= this.threshold) {\n      if (Date.now() - this.lastFailure &gt; this.resetMs) {\n        this.failures = 0; \/\/ Reset after cooldown\n        return false;\n      }\n      return true;\n    }\n    return false;\n  }\n\n  recordFailure() {\n    this.failures++;\n    this.lastFailure = Date.now();\n  }\n\n  recordSuccess() {\n    this.failures = 0;\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For monitoring your pipeline&#39;s health, see the <a href=\"https:\/\/tolinku.com\/blog\/webhook-delivery-monitoring\/\">webhook delivery monitoring guide<\/a>. For handling webhook failures and retries, see the <a href=\"https:\/\/tolinku.com\/blog\/webhook-retry-logic\/\">retry logic guide<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Process deep link events in real time with webhooks. Build streaming pipelines using queues, workers, and event-driven architectures.<\/p>\n","protected":false},"author":2,"featured_media":1164,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Real-Time Event Processing for Deep Link Data","rank_math_description":"Process deep link events in real time with webhooks. Build streaming pipelines using queues, workers, and event-driven architectures.","rank_math_focus_keyword":"real-time event processing","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-event-processing.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-event-processing.png","footnotes":""},"categories":[15],"tags":[276,20,264,272,279,294,61],"class_list":["post-1165","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-data-pipelines","tag-deep-linking","tag-engineering","tag-event-driven","tag-real-time-data","tag-streaming","tag-webhooks"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1165","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=1165"}],"version-history":[{"count":2,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1165\/revisions"}],"predecessor-version":[{"id":2264,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1165\/revisions\/2264"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1164"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1165"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1165"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1165"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}