Server-Side Deduplication
Because the engagement plugin retries delivery aggressively (sendBeacon →
fetch keepalive → a replayed localStorage queue), and because the browser
itself can silently re-deliver a sendBeacon, Silo will sometimes receive the
same event twice. Every beacon carries three identifiers: Silo uses one to drop
those retries, and you use the other two to rebuild each page view’s beacons in
order.
| Field | Location | Meaning |
|---|---|---|
messageId | top-level | UUID, unique per beacon — the dedup key for “is this the same physical send?” Collapse exact retries on this. |
page_view_id | properties | UUID, stable for one view of one URL; rolls on SPA nav (analytics.page() / popstate) and bfcache restore, not on tab-switch-back. |
sequence | properties | Integer counter, increments per beacon within a page_view_id. Order by this to reconstruct a view’s beacon stream. |
How to use them
Section titled “How to use them”- Exact retries: Silo handles these. Two sends with the same
messageIdare the same event sent twice. Silo stores eachmessageIdonce, so a retry never becomes a duplicate row in your data. - Reconstruct a page-view — group by
page_view_idand order bysequence. That yields the ordered beacon stream for a single view of a single URL: the initial dispatch, each heartbeat, and the terminal beacon. - Sum engagement correctly — within a
page_view_id, each beacon’ssecondsis the delta since the previous beacon (heartbeats use peek-then-drain), so total engaged time for the view is the sum across the sequence, capped atmaxSeconds.
Why client-side dedup isn’t enough
Section titled “Why client-side dedup isn’t enough”The plugin has an in-process dedup flag, but it can’t catch retries that happen
outside the page’s lifetime — a sendBeacon the browser re-delivers after
unload, or a queued beacon replayed on the next page load. Those only meet on
the server, which is why the identifiers travel on the wire. The server is the
authoritative dedup boundary.
Example wire shape
Section titled “Example wire shape”{ "messageId": "ajs-next-...", "properties": { "pathname": "/articles/foo", "seconds": 30, "exitType": "heartbeat", "page_view_id": "pv_...", "sequence": 2, "scroll_depth_max_px": 1840 }}