← All posts

Client-Side vs Server-Side Event Tracking: When to Use Each

“Should I track this from the client or the server?” comes up on every analytics integration, and the honest answer is both, for different events. Client-side and server-side tracking aren’t competing strategies — they’re two instruments that measure different things. The client measures what the user did and saw. The server measures what actually happened to your data and your money.

Use the wrong one and you get predictable failures: revenue numbers that don’t match the bank, conversion events that vanish because an ad blocker ate the request, or server logs that can’t tell you why a user rage-quit a screen they never technically submitted.

Here’s how to decide.

What client-side tracking is good at

Client-side tracking fires from the device — a Web/JS, iOS, Android, Unity, or WeChat Mini Program SDK running inside your app. It has one superpower: it sees the user interface and the user’s behavior in it.

Track on the client when the event is the interaction:

  • screen_view / page_view — what the user looked at
  • button_click — UI engagement
  • level_start, level_fail, tutorial_skip — in-game behavior and friction
  • ad_impression, ad_click — what the user was shown
  • session_start, session_end — engagement rhythm
  • Device and context: OS version, screen size, locale, app version

These events only exist on the client. The server never knows the user stared at the paywall for nine seconds and backed out — but that’s exactly the kind of friction signal that makes a drop-off funnel useful.

The tradeoff: the client is an untrusted, lossy environment. Requests get blocked by ad blockers and network drops, users run modified builds, clocks are wrong, and a determined user can fake any event. Never trust the client for anything that affects money, entitlements, or fraud.

What server-side tracking is good at

Server-side tracking fires from your backend — a server SDK or a direct call to the events API after your own systems have validated something. Its superpower is truth. The server only emits an event after the real thing has happened in a system you control.

Track on the server when the event must be accurate and tamper-proof:

  • purchase_complete — fire it after the payment processor or store confirms and you’ve validated the receipt, not when the user taps “Buy”
  • subscription_renew, subscription_cancel — these come from billing webhooks, not the device
  • account_create, login_success — after the auth system confirms
  • reward_grant, currency_grant — after the entitlement is actually written to the user’s record
  • Anything driven by a third-party webhook (Stripe, App Store Server Notifications, Google Play RTDN)

Server events are reliable: no ad blockers, no spoofing, no lost requests from a backgrounded app. They’re also the only honest source for revenue, because the store can refund, downgrade, or reject a purchase after the client thought it succeeded.

The tradeoff: the server is blind to the UI. It has no idea what screen the user was on or how long they hesitated. And it needs a reliable user identifier to stitch server events to the same person the client SDK is tracking.

The hybrid pattern: track intent client-side, truth server-side

The strongest setup uses both, deliberately, with distinct event names so you never confuse them.

Take a purchase. There are really two events worth capturing:

# client-side — the user tried to buy
event: checkout_start
properties:
  item_id:    "gold_pack_large"
  price_usd:  4.99
  source:     "store_paywall"

# server-side — the payment actually cleared and was validated
event: purchase_complete
properties:
  item_id:        "gold_pack_large"
  price_usd:      4.99
  payment_method: "apple_iap"
  receipt_valid:  true

Now you can build a checkout funnel from checkout_start (client, sees intent and abandonment) to purchase_complete (server, the validated truth), and your revenue reports only ever sum the server event. The gap between the two is your payment-friction metric.

This is also why a consistent event taxonomy matters: both sources should use the same snake_case property names (price_usd as a number on both, never "4.99" on one and 4.99 on the other) so the events join cleanly.

Stitching identity across both sides

The hybrid model only works if the client and server agree on who the user is. Two practical rules:

  • Establish a stable user_id as early as possible and pass it to both the client SDK and your backend. For logged-in users, this is your account ID. For pre-login, use a device or anonymous ID and alias it to the account ID at sign-up so the histories merge.
  • Set the timestamp at the moment of the event, not at ingest. Server clocks are trustworthy; client clocks aren’t, but the SDK can still send a reliable relative timestamp. Don’t let a backgrounded mobile app’s delayed flush make a Monday purchase look like it happened Tuesday.

Because Keentics keeps raw events in your own ClickHouse, client and server events land in the same store and you can join them with plain SQL — checkout_start and purchase_complete on the same user_id — instead of reconciling two disconnected dashboards.

A simple decision rule

When you’re unsure, ask one question: does this event need to be true, or does it need to capture behavior?

  • Needs to be true (money, entitlements, account state) → server-side.
  • Needs to capture behavior (clicks, views, friction, in-app actions) → client-side.
  • Want both the intent and the outcome → fire two events, one on each side, and join them.

Most products end up roughly 80% client-side (the bulk of behavioral volume) and a small but critical set of server-side events that anchor every revenue and lifecycle number you’ll ever report.

FAQ

Can’t I just track purchases client-side to keep things simple? You can, but your revenue will be wrong. The client fires when the user taps “Buy,” before the store confirms, validates the receipt, or processes refunds. Client purchase events overcount and can be spoofed. Use the client for checkout_start and the server for the validated purchase_complete.

Do server-side events lose all the device and screen context? Yes — the server doesn’t know what screen the user was on. That’s why you pass a shared user_id and capture the behavioral context client-side. At analysis time you join the two on the user, recovering the full picture.

Will ad blockers really drop my client events? A meaningful share of them, especially on web. For high-stakes conversions, this alone is a reason to confirm the outcome server-side. For ordinary behavioral events the loss is tolerable and usually statistically uniform, so trends stay valid.