If you've ever built a two-way sync between two systems and watched a record update itself repeatedly with no human touching anything, you've met a sync echo. This walkthrough covers the specific implementation of origin tagging, the fix 137Foundry's integration engineers reach for first, which stops a sync job from mistaking its own writes for new changes worth propagating.
Step 1: Confirm You Actually Have an Echo Problem
Before adding tagging infrastructure, verify the symptom. Pull the write history for one of the affected records and check whether writes are alternating between the two systems in quick succession with no meaningful field change between them. If System A writes, System B writes back a near-identical payload seconds later, and System A writes again, that pattern is the signature of an echo loop rather than legitimate concurrent editing.

Photo by Aubrey Miller on Pexels
Step 2: Pick Where the Tag Lives
You have two realistic options. The first is a native field on the record itself, a custom field or metadata property most CRMs and databases support, that carries a value like sync_origin: system_a. The second is a side-channel table you maintain yourself, mapping recent write identifiers or content hashes to which system originated them.
Native fields are simpler when available, since the tag travels with the record automatically. Side-channel tables are necessary when the platform doesn't expose a spare field, or when you don't control the schema on one end of the integration.
Step 3: Generate and Attach the Tag on Every Write
Every write your sync job makes needs a tag attached before it leaves your service. If you're using a side-channel table, this means inserting a row (write ID or content hash, origin system, timestamp) in the same transaction as the write itself, so a partial failure never leaves a write unaccounted for. A relational database like PostgreSQL makes this straightforward since the tag insert and the write can share one transaction.
def sync_write(record, target_system):
tag = generate_origin_tag(record, source_system="system_a")
record_origin_tag(tag, record.id)
response = target_system.write(record, metadata={"sync_origin": tag})
return response
Keep the tag generation deterministic where you can, derived from the record ID and a hash of the changed fields rather than a fresh random value every time. This is the same idempotence principle that makes a retried write safe, applied here to make a re-derived tag safe to compute twice without drifting.
Step 4: Check the Tag Before You Propagate
This is the step that actually stops the loop. When an inbound webhook or poll result arrives, check its origin tag (or check the side-channel table by write ID or content hash) before deciding to propagate the change further. If the tag matches your own recent write, drop the event. Don't sync it back.
def on_inbound_change(payload):
if is_own_echo(payload):
return # drop, this is our own write reflected back
process_and_sync(payload)
The order matters here. Checking the tag has to happen before any downstream processing, not as an afterthought once the change has already triggered other side effects.
Step 5: Handle Platforms That Strip Custom Metadata
Some platforms don't preserve custom fields through their own webhook payloads, even if you set the field on write. If that's the case for one side of your integration, fall back to content-hash matching in your side-channel table instead of relying on the tag surviving the round trip. Compare the inbound payload's relevant fields against what you last wrote, and treat a match as an echo regardless of whether an origin tag came through.
Step 6: Add a Short Debounce Window as a Second Layer
Origin tagging alone can miss timing edge cases, particularly when a legitimate independent edit arrives within the same narrow window as your own echo. A debounce window of a few hundred milliseconds to a couple of seconds, during which you compare incoming payloads against your own recent writes by content rather than just by tag, catches echoes that lost their tag somewhere in the pipeline. An in-memory store like Redis is a natural fit for this since the data only needs to live for the debounce window.
"Origin tagging is the fix that actually addresses the cause. Debouncing is what catches the cases where the tag didn't make it through." - Dennis Traina, founder of 137Foundry
Step 7: Log Every Tag, Matched or Not
Log both the tag you attach on outbound writes and the result of every inbound tag check, whether it matched and got dropped, or didn't match and got processed. When something eventually slips through, and in a system this stateful something eventually will, this log is what lets you find exactly where the tagging broke down instead of reconstructing it from guesswork.
Step 8: Test With a Simulated Round Trip
Standard integration tests that check "A writes, B receives it" won't catch a broken tagging implementation, because the bug only shows up on the return trip. Write a test that simulates the full loop deliberately: write to A, let the sync fire to B, simulate B's webhook firing back toward A, and assert that A's tag check correctly drops the second event instead of processing it as new.
Step 9: Watch Out for Platforms That Batch Webhooks
Some platforms batch multiple record changes into a single webhook delivery for efficiency, which complicates origin tagging if you're not expecting it. A batched payload might contain one record your sync job wrote and three records a human edited directly. Naively tagging the whole payload as an echo because one record in it matches your own recent write will silently drop legitimate changes to the other three. Unpack batched payloads and check origin tags per-record, not per-delivery.
Step 10: Don't Let Tagging Logic Live in Two Places
As a sync integration grows, it's tempting to add a second sync job, maybe for a different object type, and duplicate the tagging logic into it rather than sharing it. This works until someone fixes a tagging bug in one job and forgets the other exists. Centralize the tag-generation and tag-check logic into a shared module or service that every sync direction calls into, so a fix in one place actually fixes it everywhere the pattern is used.
A Note on Third-Party Integration Platforms
If you're using a platform like Zapier rather than building the sync yourself, you don't get direct control over origin tagging internals, but the same failure mode can still occur, a Zap that triggers on a change your own automation just made. Most platforms offer some built-in loop protection or filtering steps; it's worth explicitly checking whether that protection is enabled by default or something you have to configure, since assuming it's automatic is exactly the kind of assumption that leads to a surprise loop later.
Common Mistakes When Implementing This
A few implementation mistakes come up often enough to call out directly. Generating a fresh random tag on every write instead of a deterministic one makes it impossible to recognize the same logical change computed twice, which defeats part of the purpose. Checking the tag after other side effects have already fired, rather than as the very first step in handling an inbound change, means the echo still causes damage even though it eventually gets recognized. And forgetting to test the actual round trip, rather than just each direction in isolation, is how a tagging bug ships to production looking like a fully tested feature.
Wrapping Up
Origin tagging is a small amount of code with an outsized effect on integration stability. The pattern is the same regardless of which platforms you're connecting: generate a tag on write, check it before you propagate, and back it up with a short debounce window for the cases the tag alone won't catch. If you want the fuller architecture, including idempotency keys and conflict resolution alongside origin tagging, this guide on building two-way sync without an infinite loop covers the whole system end to end.
Top comments (0)