Workflow Automation

The label printer that fired twice

A shared log, debounce window, and SKU + price key stop duplicate label prints while exposing a deeper product identity bug.

Octacer July 6, 2026 8 min read
A single price sticker resting on a dark surface beside a stalled label printer, with faint duplicate outlines fading away and no readable text

The label printer that fired twice

A sports-cards e-commerce client kept finding two identical price stickers where one should have printed. The label workflow was doing exactly what it was told — it was told twice. We fixed it with a small log, a short pause, and one rule: for a given SKU and price, only the earliest run gets to print.

Then we found the deeper reason the duplicates existed at all, and it had nothing to do with the printer.

The problem

The client runs a label-printing workflow in n8n. When the storefront (Shopify) signals a product or price event, the workflow prints a physical sticker for that item. One event, one sticker. That is the whole contract.

In production, the storefront sometimes sent the same event twice in quick succession — a double-fire. The workflow had no reason to doubt the second event, so it printed a second sticker. Staff pulled duplicate stickers off the printer, had to notice the duplication by eye, and had to throw the extra away. Every double-fire became a small manual cleanup and a moment of "did this price actually change twice?"

The cost was not dramatic, but it was constant and it eroded trust in the automation. A workflow that prints phantom labels is a workflow people start double-checking, which defeats the point of automating it.

Why the obvious fix didn't work

The obvious fix is "ignore repeat events." But you cannot naively drop the second event, because two events for the same SKU are not always duplicates. A real reprice is also two events for the same SKU — and that one should print.

So a filter keyed on SKU alone is wrong. It would suppress legitimate reprice labels, which is a worse failure than an occasional duplicate. You would trade a visible, easy-to-catch problem (an extra sticker) for an invisible one (a missing sticker on a genuinely repriced item).

Deduplicating inside a single event also does not help. Each double-fire arrives as its own independent execution of the workflow. There is no shared memory between the two runs unless we create one. The two executions do not know about each other, so neither can decide "I am the copy."

We needed something that could tell a duplicate apart from a reprice, and something the separate runs could both see.

What we did

We gave the runs a shared memory and a tie-breaker.

Just before the print step, the workflow writes a row to a data table: the SKU, the price, and the current time. Then it deliberately pauses for a few seconds. During that pause, any duplicate event for the same item is also writing its own row. When the pause ends, the run reads the log back and looks at every row matching the same SKU and the same price.

If this run holds the earliest timestamp for that SKU + price, it continues and prints. If an earlier row already exists, this run is a duplicate and skips the print. Earliest wins; everyone else stands down.

The key choice is the dedup key: SKU + price, not SKU alone. That single decision is what separates duplicates from reprices. Two double-fire events carry the same SKU and the same price, so they collapse to one print. A reprice carries the same SKU but a different price, so it writes a separate log entry, finds no earlier row for that SKU + price, and prints normally. The reprice label still happens, exactly as staff expect.

The pause is the debounce. It is the window in which near-simultaneous events can all record themselves before anyone commits to printing. Without it, the first run could read the log and print before the duplicate had even written its row — and the duplicate, reading later, would see itself as earliest too.

How it works

The mechanism is four steps: log, debounce, read back, earliest-wins.

Duplicate label events collapse to a single print
on print request (SKU, price, now):
    write row -> data_table { sku, price, ts: now }
    wait a few seconds            # debounce window

    rows = data_table.where(sku == SKU and price == price)
    earliest = min(rows, key = ts)

    if earliest.ts == this_run.ts:   # I am the earliest for SKU+price
        print_label(SKU, price)
    else:
        skip()                       # a duplicate already won

Walk the diagram. Two events for the same SKU and price arrive nearly at once. Both write a row into the data table, each stamped with its own time. Both then wait out the debounce window. When the window closes, both read the same set of rows — because both rows are now present. Both compute the earliest timestamp, but only one run's own timestamp equals it. That run prints. The other sees an earlier row than its own and skips.

For a reprice, the second event lands in a different bucket entirely. Its price differs, so data_table.where(sku == SKU and price == price) returns only its own row. It is trivially the earliest of that set, so it prints. The dedup key does the discriminating; the debounce only ensures all contenders for the same key are on the board before anyone decides.

What broke / what surprised us

The debounce stopped the double-print. But it treated a symptom. The question we kept asking was: why were duplicate events being generated in the first place?

The answer sat one system upstream, and it surprised us.

  1. Title change

    A SKU was created in the storefront with a certain keyword in its product title. It synced to the ERP (NetSuite) as one record and printed one label. Fine. Later, the same product was recreated without that keyword in the title. The storefront handed the downstream systems a different item id for what a human would call the same product. The ERP saw a new item id, decided this was a brand-new thing, created a second record — and a second label printed.

  2. Identity break

    The duplicate was not a race condition at all in that case. It was an identity bug. The product's identity was effectively being derived from its title content, which is mutable. Change the words in the title, and the system thinks you changed the product. Stable-looking things (the physical card, the SKU in a person's head) were riding on an unstable key (an id that shifted when the title changed).

  3. Root cause

    This reframed the whole incident. The debounce protects against the storefront firing the same event twice. But it cannot protect you when two genuinely different item ids describe one real-world product, because those are not duplicates by any key you would trust — they are two records that should have been one. The only real fix for that class of problem is upstream: identity has to come from a stable key that does not move when descriptive fields change.

Results

The debounce is running in production and behaving as expected. We verified it under real traffic and monitored it: double-fire events for the same SKU and price now collapse to a single sticker, and genuine reprices still print their own label. The manual "spot the duplicate and bin it" step is gone for the double-fire case.

We measured this by watching the data table's own log against printer output during monitoring — every matched SKU + price group resolves to exactly one print, with later rows recorded but skipped. Because the log is the same table the workflow reads, the audit trail and the dedup decision are the same artifact; there is no separate instrumentation to drift out of sync.

The identity-driven duplicates are a separate, upstream fix. Naming them explicitly matters: it tells everyone that a duplicate label with two different item ids is not a printer problem and will not be caught by the debounce.

Takeaways

  1. Dedup on the key that means "the same thing happened," not "the same subject." SKU alone conflates a reprice with a double-fire. SKU + price is the identity of the event, and that is what makes the filter safe. Pick the key that distinguishes the cases you must keep apart.
  2. Idempotency needs shared state and a deterministic tie-breaker. Independent executions cannot deduplicate from memory they do not share. A log they both write to, plus "earliest timestamp wins," turns two blind runs into one decision.
  3. A debounce is a window, not a guess. The pause exists so every contender for a key can register before anyone commits. Skip it and the first run prints before the duplicate has recorded itself — and idempotency quietly fails.
  4. Never derive identity from mutable content. A product id that changes when the title changes is not an identity; it is a description. Anchor identity to a stable key so editing a field never spawns a second record.
  5. Fix the symptom fast, but name the root cause louder. The debounce bought immediate relief. The lasting lesson is upstream: stable identity keys prevent the duplicates the debounce can never see.

Ready to Implement These Strategies?

Let's discuss how to apply these insights to your specific business challenges.

Schedule Consultation