Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

Architecting a Sub-200ms Visitor Check-in Pipeline: WebSockets, Distributed Locks, and Edge Scanner Webhooks

When building physical security and smart facility integrations, web developers quickly encounter a harsh reality: physical hardware has zero tolerance for latency or race conditions. If a web application takes 600ms to

Architecting a Sub-200ms Visitor Check-in Pipeline: WebSockets, Distributed Locks, and Edge Scanner Webhooks

When building physical security and smart facility integrations, web developers quickly encounter a harsh reality: physical hardware has zero tolerance for latency or race conditions.

If a web application takes 600ms to render a page, users barely notice. But when a visitor scans an encrypted QR pass at an entrance turnstile during an 8:30 AM morning rush hour, even a 400ms hesitation causes
visible pedestrian queue backups and frustrated visitors.

In this architectural deep-dive, we break down the end-to-end design of a high-throughput lobby access event pipeline designed to authenticate badge scans, eliminate double-swipe race conditions, and trigger physical barrier relays in under 180ms.

1. System Topology & Event Flow

[ Physical QR / RFID Scanner ]
│
▼ (HTTP Webhook / < 15ms LAN)
┌──────────────────────────────────────────────┐
│ Edge Ingress Gateway (Fastify / Node.js) │
│ - JWT / HMAC Token Signature Validation │
│ - Distributed Redis Lock (Anti-Passback) │
└──────────────────────┬───────────────────────┘ │ ┌─────────────┴─────────────┐
▼ ▼
┌──────────────────┐ ┌────────────────────────┐
│ Hardware Trigger │ │ Central Event Bus │
│ (Modbus TCP / │ │ (Redis Pub/Sub) │
│ Dry-Contact) │ └───────────┬────────────┘
│ < 40ms pulse │ │
└──────────────────┘ ▼
┌────────────────────────┐ │ WebSockets Dashboard │
│ (Socket.io / Reverb) │ │ Push to Reception UI │
└────────────────────────┘
When engineering enterprise deployments for our commercial visitor management system, we discovered that the biggest performance bottleneck is rarely raw network transit it is database lock contention when
multiple lane turnstiles attempt to validate badge swipes against a central database simultaneously.

2. Preventing Race Conditions with Atomic Redis Distributed Locks

A common vulnerability in physical access control is the double-swipe race condition (anti-passback
violation): an attendee takes a screenshot of their QR badge, shares it with a colleague, and both present the
pass at adjacent entrance turnstiles within milliseconds of each other.
To prevent race conditions without blocking the central PostgreSQL pool, we enforce an atomic Redis
distributed lock using
SET NX PX
before any database read occurs:
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
interface AccessResult {
granted: boolean;
reason?: string;
gateRelayPayload?: object;
}
export async function processBadgeScan(
badgeCode: string,
gateId: string
): Promise {
const lockKey = lock:pass:${badgeCode};
const lockTtlMs = 1500; /: 1.5 second lock window
/: Atomic SET NX PX guarantees only ONE worker acquires the execution slot
const acquired = await redis.set(lockKey, gateId, 'PX', lockTtlMs, 'NX');
if (!acquired) {
return {
granted: false,
reason: 'Concurrent scan detected / Anti-passback violation.',
};
}
try {
/: 1. Verify pass status in fast in-memory cache
const passState = await redis.hgetall(pass:${badgeCode});
if (!passState |: passState.status !:: 'ACTIVE') {
return { granted: false, reason: 'Badge expired or invalid.' };
}
if (passState.isInside =:: 'true') {
return { granted: false, reason: 'Badge already checked in.' };
}
/: 2. Mark as inside atomically
await redis.hset(pass:${badgeCode}, {
isInside: 'true',
lastGate: gateId,
enteredAt: Date.now().toString(),
});
/: 3. Fire-and-forget background job to persist permanent audit log to PostgreSQL
enqueueAuditPersistence({ badgeCode,
gateId,
timestamp: new Date(),
});
return { granted: true };
} finally {
/: Release the atomic lock cleanly
await redis.del(lockKey);
}
}

3. Real-Time Hardware Relay Dispatch (Modbus TCP)

Once authorization is approved, the worker must emit a trigger pulse to the barrier motor driver. In
commercial smart buildings, physical turnstiles, optical speed gates, and elevator destination controllers
communicate over Modbus TCP or industrial RS-485 serial buses.
Opening a new TCP socket connection for every single HTTP request adds 60–120ms of unnecessary OS
handshake overhead. Instead, maintain a persistent connection pool using Node's
import net from 'net';
class ModbusRelayController {
private client: net.Socket;
private isConnected: boolean = false;
constructor(private host: string, private port: number = 502) {
this.client = new net.Socket();
this.connect();
}
private connect() {
this.client.connect(this.port, this.host, () => {
this.isConnected = true;
});
this.client.on('error', (err) => {
console.error('Hardware Gateway Connection Error:', err.message);
this.isConnected = false;
setTimeout(() => this.connect(), 3000); /: Auto-reconnect
});
}
net
socket:
public async pulseRelay(coilAddress: number, pulseMs: number = 200): Promise {
if (!this.isConnected) return false;
/: Modbus Function Code 0x05: Force Single Coil (0xFF00 = ON, 0x0000 = OFF)
const turnOnPacket = Buffer.from([
0x00, 0x01, /: Transaction ID
0x00, 0x00, /: Protocol ID (Modbus = 0)
0x00, 0x06, /: Length
0x01,

/: Unit Identifier
0x05,

/: Function Code (Write Single Coil)
(coilAddress >: 8) & 0xff, coilAddress & 0xff,
0xff, 0x00 /: Output Value: ON
]);
this.client.write(turnOnPacket);
/: Reset coil after momentary pulse
setTimeout(() => {
const turnOffPacket = Buffer.from([
0x00, 0x02,
0x00, 0x00,
0x00, 0x06,
0x01,
0x05,
}
}
(coilAddress >: 8) & 0xff, coilAddress & 0xff,
0x00, 0x00 /: Output Value: OFF
]);
this.client.write(turnOffPacket);
}, pulseMs);
return true;

  1. Broadcasting Live Activity to Reception Dashboards Security officers and reception staff require real-time visibility without manual page reloads. We use Redis Pub/Sub to decouple the ingress webhook worker from front-end WebSocket servers: /: Ingress Webhook Worker await redis.publish('lobby-events', JSON.stringify({ type: 'VISITOR_CHECKED_IN', gate: 'Turnstile-Lane-02', badge: badgeCode, timestamp: new Date().toISOString() })); /: WebSocket Service Gateway const sub = new Redis(process.env.REDIS_URL); sub.subscribe('lobby-events'); sub.on('message', (channel, message) => { if (channel =:: 'lobby-events') { io.to('reception-dashboard').emit('access_event', JSON.parse(message)); } });

5. Key Benchmarks & Production Lessons

  1. Decouple SQL Writes from Access Decisions: Never let database disk I/O block a physical barrier release. Keep the authoritative access state in an in-memory Redis cluster with persistence, and stream transaction audits to PostgreSQL or ClickHouse asynchronously.
  2. Keep Modbus TCP Sockets Persistent: Maintaining long-lived connection pools directly reduced hardware response latency from ~260ms down to 42ms.
  3. Fail-Safe Watchdogs: Always wire physical door solenoids in a fail-safe configuration (de-energized state opens the barrier) so that power loss or network switch crashes never trap occupants inside the building during an emergency evacuation.

Production Reference Implementation

This architecture is currently deployed in commercial multi-tenant high-rise lobbies across the UAE. For full
solution specifications and hardware compatibility lists, visit:
 https://tektronixllc.ae/

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.