How to Build a Reliable Analytics Event Pipeline

Queues, outboxes, retries, event IDs, deduplication, and reconciliation for analytics events you cannot afford to lose silently.

JR4 min read

The first version of analytics is usually a fetch call in a request handler. The business action succeeds, the app sends an event, and a chart moves. That is enough to learn the shape of the problem.

It is not a reliable production pipeline.

Networks time out. Deployments terminate processes. Providers throttle bursts. A server can deliver an event and lose the response, leaving it unsure whether retrying will create a duplicate. The important design question is not “How do I call the API?” It is “What should happen between the business fact being committed and the analytics destination acknowledging it?”

Classify the event first#

Not every event needs the same delivery guarantees.

A debug breadcrumb can be best effort. A feature impression may tolerate occasional loss. A subscription change should be queued, retried, and reconciled. Billable usage belongs in an authoritative ledger even if analytics receives a copy.

Write the failure policy before choosing machinery. If losing 1% of an event would change a business decision, an unobserved background promise is not enough.

Analytics should also remain downstream of the business. A checkout should not fail because the charting service is unavailable, and a GraphJSON row should not decide whether an invoice is paid.

Create identity before delivery#

Important events need a stable identifier at the source:

{
  "event": "invoice_paid",
  "event_id": "evt_01J2Y9DA7W",
  "account_id": "acct_7",
  "invoice_id": "in_91",
  "amount": 4900,
  "currency": "usd"
}

Generate event_id when the business action occurs, not when a worker happens to pick up the job. Every retry keeps the same value.

This gives you something to reconcile and deduplicate. Without it, two identical-looking rows may be a legitimate repeated action or one uncertain delivery retried twice.

Preserve the original occurrence time too. A backfill delivered today should still appear in the period when the action happened.

Use a transactional outbox for important facts#

The strongest pattern writes the business change and a pending analytics event in one database transaction:

request
  ├─ update business state
  └─ insert outbox row
          ↓
      background worker
          ↓
      analytics API

If the transaction commits, the event is available for delivery. If it rolls back, neither the state nor the event exists. A worker can restart without asking the original user request to run again.

A platform queue is also useful, especially in serverless systems, but be explicit about the narrow moment between committing business state and publishing the message. The outbox exists to close that gap.

For low-value events, you may decide this is too much infrastructure. That is a valid tradeoff when it is named and measured.

Retry only what can recover#

Retry network failures, timeouts, throttling, and server errors with exponential backoff and jitter. Do not retry a malformed payload unchanged. That only turns one bad event into a noisy queue and hides the real problem.

Every retry policy needs:

  • a per-attempt timeout
  • a maximum number of attempts
  • a total delivery deadline
  • bounded worker concurrency
  • a dead-letter destination

Dead letters are not successful delivery. They need an owner, an alert, a redacted error reason, and a replay process after the underlying problem is fixed.

Batching makes delivery more efficient. GraphJSON accepts up to 50 events in one bulk request, so a worker can reduce connection overhead without creating an enormous failure unit.

Design for at least once#

Most analytics ingestion paths cannot offer exactly once across an unreliable network. Suppose the destination accepts a batch, but the response disappears. The sender can drop the batch and risk data loss, or retry and risk a duplicate.

For important events, retry. Then make duplicates manageable:

  1. preserve stable event_id
  2. keep retry payloads immutable
  3. count unique event IDs in critical queries
  4. monitor the duplicate rate

At-least-once is not a defect. It is an honest delivery model with a defined query strategy.

Reconcile the pipeline#

A successful HTTP response proves one request was accepted. It does not prove every business fact made it from the source to the destination over the month.

Compare:

  • source records created
  • outbox or queue jobs published
  • batches acknowledged
  • dead-lettered events
  • unique analytics event_id values
  • totals such as revenue by currency

Use a settled interval and account for expected exclusions. Sample entire customer journeys, not only aggregate counts. A missing identity field can preserve row counts while breaking funnels and retention.

Monitor queue age as well as queue size. Ten events waiting for six hours may be more serious than a thousand events waiting for six seconds.

Keep the user path clean#

The user-facing transaction should usually:

  1. commit the business action
  2. make the event durable
  3. return success

The worker should:

  1. batch compatible events
  2. deliver with timeouts and bounded retries
  3. acknowledge only after success
  4. quarantine permanent failures
  5. expose metrics and alerts

“Analytics must not break the product” should never become “analytics failures are silently ignored.”

The complete implementation, sample worker, retry matrix, and delivery checklist are in Build a reliable event delivery pipeline.

JR

Written by JR

Founder and builder of GraphJSON.