A scan screen that writes each barcode to a table is simple until the same code lands twice. The comparison against the last scan looks airtight, yet every few shifts a duplicate row appears, and the bug never recreates at your desk. That is the classic signature of a race between the property change event and the script that handles it. Here you will find the two races that let a second scan through, the one-line fix that reads the scan from the event itself, the timing arithmetic that shows how wide the window really is, and the screen changes that stop operators from re-scanning in the first place.
Symptom: the duplicate only shows up hours into a shift
The screen keeps the scan field in focus, and every text change fires the script: read the field, compare against the code stored in the 'compare' tag, insert the row if the two differ, then update that tag. On paper a second identical scan can never insert. In production it does, a few times a week, always seconds after the first scan. Every attempt to recreate it at the desk produces exactly one row.
That combination points at a race. The comparison logic is fine; the values it compares are not the ones it thinks it is comparing. Two separate races are in play, and both are quick to fix.
Cause 1: the value you compare is not the scan that fired
The event fires first; your script runs later, on the platform's schedule. Reading event.source.text inside the handler returns whatever the field holds at that later moment. If the next scan's change has already landed on the field, you process a value that belongs to a different event, and the record you log is not the scan that triggered the insert.
The event object solves this. It carries its own snapshot of the change: the name of the property that changed and the value it changed to. Read event.newValue and each handler judges exactly the scan that woke it.
Watch for the cousin of this bug: a field that keeps its text and lets the next scan append produces a longer string every time, which every comparison sees as new. That is a terminator problem, not a race, and barcode scanner appends to the previous value: fix it with selectAll covers it.
Cause 2: the gate closes after the slow insert
The comparison tag is written last, after the database insert returns. Everything the handler does between reading the old tag and writing the new one happens with the gate open. During those hundreds of milliseconds the tag still holds the previous code, so a second pull of the same code compares against the old value, finds a difference, and passes as new. The wider the gap between the read and the write, the wider the window.
The fix: read the event value, close the gate first
The fix the field confirmed is one line: read event.newValue instead of the component property. A second change closes the timing race too: write the compare tag before the insert call.
# before: reads the field late, closes the gate after the insert
if event.PropertyName == 'text':
scan = event.source.text # str, field value at read time
lastScan = system.tag.read('compare').value
if scan != lastScan:
insert_row(scan) # your database insert
write_compare_tag(scan) # gate closes here
# after: the event's own value, gate closed before the slow work
if event.PropertyName == 'text':
scan = event.newValue # str, value this event fired for
if scan: # ignore the empty clear
lastScan = system.tag.read('compare').value
if scan != lastScan:
write_compare_tag(scan)
insert_row(scan)
Here insert_row and write_compare_tag stand for your existing calls; event.PropertyName, event.source.text, event.newValue and the 'compare' tag path are the names the screen already uses.
Worked example: two pulls of the trigger 180 ms apart
Inputs, with their sources:
- Re-pull interval: 180 ms — an assumed bench value for an operator pressing the trigger again because nothing appeared; the field reports exactly this habit.
- Tag read: 5 ms — assumed, local tag service.
- Database insert: 210 ms — assumed, typical LAN insert with commit.
- Tag write: 10 ms — assumed, local tag service.
- Handler order: the platform runs queued property change handlers in order, oldest first.
The rule in words: the gate is open from the moment the first scan's handler reads the compare tag until the moment it writes the tag; a duplicate arriving in between reads the stale key and passes.
The arithmetic, original order:
open window = tag read + insert + tag write
= 5 ms + 210 ms + 10 ms
= 225 ms
The second pull lands 180 ms after the first, inside the 225 ms window, so the handler reads the old key, sees a difference, and inserts a second row.
The arithmetic, fixed order:
open window = tag read + tag write
= 5 ms + 10 ms
= 15 ms
The tag now holds the code from 15 ms onward. The 180 ms re-pull reads the current key, finds a match, and is rejected.
The decision: with the insert before the write, any re-scan sooner than 225 ms gets through, and a deliberate double pull sits comfortably inside that. Reordered, the threshold drops to about 15 ms, faster than a person can re-trigger. The insert time is the number that moves the window: every 100 ms added to the database round trip widens the original window by 100 ms, so a remote database inserting in 360 ms takes it to 375 ms. If your platform runs queued handlers concurrently instead of in order, both handlers can still read the stale key; the session-property guard in the prevention section closes that case.
When the scan comes from a fixed code reader on the line
On a line, the scan that doubles is often not a human. A reader aimed upstream re-decodes the same label on every frame until the part moves out of view, and each decode arrives at the screen as a fresh message. The LS-CRD-0028 captures 2560 x 2048 frames at 30 fps and decodes up to 60 codes/s.
- Interface
- Fast Ethernet
- Resolution
- 5 MP
- Sensor format
- 1/1.7"
- Shutter
- Global
That rate is why one part can reach your screen many times over. Handle it at the source: configure the reader to report a code once per part, so the screen's gate only has to judge human re-scans. Where the station is tight on space, the MV-ID3013PM-12M-WBN pairs a 1.3 MP sensor with a 12 mm focal length in a compact M12-mount body.
- Interface
- Fast Ethernet
- Resolution
- Up to 3 MP
- Shutter
- Global
- Lens mount
- M12
If you are choosing hardware for the station, the fixed code readers and smart cameras pages filter by interface, resolution and mount, and the camera finder narrows by the same fields. A smart camera can run the read and the one-code-per-part rule on the device, so nothing duplicated ever reaches the screen.
Verify it on the bench
- Log both values for a shift. Append event.newValue and event.source.text, each with a millisecond timestamp, to a test log on every text change. A shift with a double pull shows whether the two values ever diverge; divergence confirms cause 1.
- Fire two scans 150 ms apart, with a second scanner or a scripted field update. With the fix in place the table must gain exactly one row. Run it twenty times.
- Re-scan after the gate closes. The same code two seconds later must be rejected while the compare tag holds it, which proves the gate and not just the ordering.
- Pull the database out. Drop the network during a scan: the gate stays shut, the retry path fires, and the screen tells the operator the scan failed instead of losing it.
Prevention: make the screen prove it heard the scan
The re-scan habit has a root cause: the operator cannot see the row appear, so they scan again to make sure. Write the accepted code to the on-screen table the moment the gate passes, not after the insert returns, and the urge to re-pull mostly disappears.
Three more simple guards:
- Hold the last code in a session property and compare against it first; it covers the case where queued handlers run out of order, with the compare tag kept as the persistent record.
- A unique index over code, station and time window in the table is the backstop that catches a duplicate no matter which client fails; stop duplicate barcode scans in a batch table: event over polling covers the table side.
- If the double fire comes from the component tree rather than a second scan, scan barcode action fires twice: fix inherited container events is the adjacent case.
Run the bench checks above, then wire the station's hardware through the system configurator or send the screen's behavior and the timeline from the worked example to our support team. More screen-side scan patterns sit on the systems topic page.
Questions
Why can I not recreate the double scan at my desk?
Because the failure needs two events inside the gate's open window, typically well under a quarter of a second. At a desk you scan once, wait, watch the row appear, then scan again, and the gate has long since closed. Put a scripted second text change 100 to 200 ms after the first on a test screen and the original code duplicates on demand.
Does reading event.newValue require changing the scanner?
No. The fix is screen-side only: the event object hands your script the value that fired it, whatever the scanner sends. Still check the scanner so one trigger pull sends exactly one message with its terminator; a reader set to transmit the code twice produces two real events, and the compare tag is what keeps the second one out of the table.
How soon can the same code be scanned legitimately again?
The compare tag holds one value, so the gate rejects that code until something overwrites it. If the same code must be accepted later, such as a pallet returning to the same station, clear the compare tag on the event that ends that pass, like a part-present sensor or a station cycle signal, instead of trying to time it.
Should the duplicate check also live in the database?
Yes, as a backstop. A unique index over the code, the station and a time window stops a duplicate row no matter which client fails. Keep the screen-side gate too: it rejects the repeat scan in milliseconds with no round trip, while the database constraint only tells you after a bad row has tried to land.
The scan appends to the text already in the field. Is that the same race?
No. Appending means the scanner's end-of-message character is not clearing or submitting the field, so each scan concatenates onto the last one and every comparison sees a longer, new-looking string. Fix the field's submit or clear behavior first; the race fix then applies to single, clean scans.

