Building an Event-Driven IoT Smart Parking System: Real-Time Sensor Processing, Node.js Microservices, and QR Billing

["IoT""Node.js""Socket.io""Express.js""Redis"]

Finding parking in busy metropolitan hubs is a constant friction point for commuters. Traditional parking garages rely on manual ticketing, slow barriers, and opaque slot availability, leading to long queues and blockages.

To solve this, we built a full-stack, event-driven IoT Smart Parking Platform. By combining hardware simulations, real-time message streams, and automated payment gateways, the system reduced lot congestion and waiting times by 80%. In this post, we’ll look at the backend architecture that powered this system.


High-Level System Architecture

The core requirement of a smart parking system is real-time synchronization. If slot availability is out-of-sync by even a few seconds, two drivers might try to park in the exact same spot.

 [IoT Gate / Sensors] ---> [MQTT / HTTP Broker] ---> [Node.js Backend] ---> [Redis Cache]
                                                              |
                                                              v
 [React Web Client] <--- [WebSockets (Socket.io)] <-----------+

Our architecture consists of four distinct layers:

  1. IoT Sensor Emulators: Nodes simulating ultrasonic sensors at each parking slot, broadcasting status changes (occupied vs. free) via lightweight JSON payloads.
  2. Node.js/Express.js Backend: The core orchestrator handling space reservations, user profiles, and transaction records.
  3. Redis Caching & Locking: Used to prevent double-bookings and handle fast slot-state caching.
  4. WebSocket Stream (Socket.io): Broadcasting real-time map updates to the React client UI.

Real-Time Slot Streaming with Socket.io

When an ultrasonic sensor detects a state change, it hits our backend ingestion endpoint. Rather than forcing clients to poll the database, the server pushes the updated grid coordinates to all active clients immediately.

Here is the Express configuration handling slot ingestion and WebSocket broadcasts:

const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
 
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
  cors: { origin: "*" }
});
 
app.use(express.json());
 
// Ingest Slot State Changes from IoT Gateways
app.post('/api/sensors/status', async (req, res) => {
  const { slotId, garageId, status } = req.body; // status: 'OCCUPIED' or 'FREE'
 
  try {
    // 1. Update Database state (e.g. MongoDB/PostgreSQL)
    await updateSlotStateInDb(slotId, status);
 
    // 2. Publish updated slot state over WebSockets to garage-specific room
    io.to(`garage_${garageId}`).emit('slot_update', {
      slotId,
      status,
      updatedAt: new Date()
    });
 
    return res.status(200).json({ success: true, message: "Slot updated" });
  } catch (error) {
    console.error("Sensor update error:", error);
    return res.status(500).json({ error: "Failed to process sensor state" });
  }
});
 
io.on('connection', (socket) => {
  // Join a room for a specific parking garage structure
  socket.on('join_garage', (garageId) => {
    socket.join(`garage_${garageId}`);
    console.log(`Client joined garage room: ${garageId}`);
  });
});
 
server.listen(3001, () => console.log('Ingestion server listening on port 3001'));

Handling Race Conditions with Redis Locks

One of the hardest problems in booking systems is the thundering herd issue. If a parking spot becomes free in a highly congested garage, multiple drivers might tap "Book Space" at the exact same millisecond.

If we handle this with traditional database queries:

  1. Driver A checks database: Spot is free.
  2. Driver B checks database: Spot is free.
  3. Driver A writes booking.
  4. Driver B writes booking (overwriting or creating a double-booking).

To avoid this, we introduced a distributed lock using Redis before writing the booking to our primary database.

const Redis = require('ioredis');
const redis = new Redis();
 
async function bookParkingSpot(userId, slotId) {
  const lockKey = `lock:slot:${slotId}`;
  
  // Attempt to acquire an exclusive lock in Redis (expires in 5 seconds to prevent deadlocks)
  const acquired = await redis.set(lockKey, userId, 'NX', 'PX', 5000);
 
  if (!acquired) {
    throw new Error("Spot is currently being locked by another booking attempt.");
  }
 
  try {
    // Check if slot is already occupied in DB
    const isFree = await checkSlotAvailability(slotId);
    if (!isFree) {
      throw new Error("Spot is already occupied.");
    }
 
    // Process reservation
    const booking = await createBookingRecord(userId, slotId);
    return booking;
  } finally {
    // Release the lock only if we own it
    const lockOwner = await redis.get(lockKey);
    if (lockOwner === userId) {
      await redis.del(lockKey);
    }
  }
}

This ensures that bookings are serialized. If two drivers hit the button simultaneously, the Redis set NX guarantees that only one request acquires the key first, while the other is immediately rejected with a clean message to choose another spot.


Automated QR Code Entry & Exit Control

To speed up entry, the client React app generates a dynamic, secure QR code when a booking is confirmed. The QR code doesn't just contain the reservation ID—it wraps a temporary JWT (JSON Web Token) signed with a server-side secret containing the reservation metadata and an expiration timestamp.

When the vehicle reaches the entry gate:

  1. The driver scans the QR code at the gate terminal.
  2. The gate reader decodes the JWT and sends it to the backend validator.
  3. The server checks the signature and expiration.
  4. If valid, the server triggers the barrier relay to open, fires a GATE_ENTERED event to the database, and begins the reservation timer.
  5. Upon exit, a similar scan triggers automatic fee deduction via Stripe/Razorpay and immediately marks the slot as FREE, opening the spot for new drivers.

Impact & Takeaways

By replacing manual ticketing with WebSockets and JWT QR-billing, we removed the friction of physical ticketing. Drivers knew exactly where to park before entering, reducing searching and congestion in the garage structure by 80%.

For full-stack developers building real-time IoT platforms, Node.js remains an exceptional runtime. Its single-threaded non-blocking event loop handles thousands of concurrent WebSocket connections and sensor webhooks with minimal memory footprint, making it perfect for scaling municipal infrastructure.