IoT Automation

Smart Parking IoT with MQTT: Bay Sensors, Occupancy Counts and a Live Availability Map

A practical blueprint for smart parking with IoT: choose bay sensors, design MQTT topics, aggregate free spaces per zone and drive a live browser map over secure WebSocket.

Smart parking IoT diagram with bay sensors in a car park publishing occupancy to a live availability map
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

Smart parking IoT systems tell drivers where the free spaces are before they start circling, and tell operators how their car parks are actually used. This article walks through a complete design: choosing bay sensors, structuring MQTT topics, aggregating free spaces per zone, and driving a live map in the browser that updates the moment a car leaves.

What a Smart Parking System Has to Do

Strip away the marketing and a parking guidance system has four jobs:

  1. Detect whether each bay is occupied.
  2. Transport those state changes reliably from the field to the back end.
  3. Aggregate individual bays into numbers people care about: free spaces per level, per zone, per car park.
  4. Display the result on signs, apps and maps, in near real time.

MQTT fits the transport and display layers naturally. Each bay is a retained state topic, aggregators subscribe with wildcards, and browsers subscribe over WebSocket for live updates. The rest of this article follows the data from the asphalt to the screen.

Choosing Bay Sensors

Sensor type How it works Strengths Watch out for
Magnetometer (in-ground or surface puck) Detects the change in Earth's magnetic field caused by a car Long battery life, unaffected by light or weather Interference from adjacent bays, needs calibration
Radar (combined with magnetometer) Short-range reflection from the vehicle underside Fewer false positives than magnetometer alone Higher cost and power
Ultrasonic (ceiling mounted) Measures distance to the roof of the car Simple, common in indoor garages, often with LED indicator Needs mains power and indoor mounting
Camera with vision model One camera covers many bays Low cost per bay, can read plates Privacy rules, lighting, processing at the edge

For open-air car parks and kerbside bays, battery-powered magnetometer or radar pucks on LoRaWAN or NB-IoT are the usual choice. Indoor garages often use ceiling ultrasonic sensors on a wired bus with a local gateway. Camera systems publish results rather than video: the edge device runs detection and publishes only occupied or free per bay.

Connectivity Paths to the Broker

How sensors reach the broker shapes the architecture more than the sensor type:

  • LoRaWAN: sensors talk to gateways, gateways forward to a network server such as The Things Stack or ChirpStack, and the network server, or a small bridge service, publishes decoded data to your broker. Hundreds of bays become a single MQTT connection. See The Things Network with MQTT for the bridging step.
  • NB-IoT or LTE-M: each sensor can connect directly over MQTT, or report via the operator's platform which you then bridge.
  • Wired bus plus gateway: an indoor gateway polls ultrasonic sensors and publishes changes.
  • Edge camera: a small computer runs detection and publishes per-bay results.

The gateway approach is attractive beyond cost. It keeps connection counts low, so even a small broker plan handles a large car park, and the bay state is still a clean MQTT topic by the time it reaches your applications.

Topic Design for Smart Parking IoT

A hierarchy that mirrors the physical layout makes every query a wildcard:

parking/<site>/<level>/<bay>/state          retained: {"occupied":true,"ts":1760600000}
parking/<site>/<level>/<bay>/battery        retained: 3.41
parking/<site>/<level>/summary              retained: {"free":37,"total":120}
parking/<site>/summary                      retained: {"free":112,"total":420}
parking/<site>/<level>/<bay>/availability   retained online/offline (gateway-reported)
  • parking/central/+/+/state is every bay in the central car park.
  • parking/+/summary is the headline number for every site, which is all a city-wide sign needs.

Retaining both bay states and summaries means a new app or sign shows correct numbers immediately on connect. Include a timestamp in the payload, because a bay that has not reported for a day is "unknown", not "free".

Aggregating Free Spaces per Level

An aggregator subscribes to all bay states, keeps them in memory and republishes summaries whenever something changes. This Python version uses paho-mqtt 2.x and the public test broker:

import json
import time
from collections import defaultdict
import paho.mqtt.client as mqtt

PREFIX = "connectmq/demo/p3v6z8"         # your own random prefix
SITE = "central"
STALE_AFTER = 6 * 3600                    # seconds without a report = unknown
bays = {}                                 # (level, bay) -> {"occupied": bool, "ts": float}

def publish_summaries(client):
    now = time.time()
    per_level = defaultdict(lambda: {"free": 0, "occupied": 0, "unknown": 0, "total": 0})
    for (level, _), s in bays.items():
        bucket = per_level[level]
        bucket["total"] += 1
        if now - s["ts"] > STALE_AFTER:
            bucket["unknown"] += 1
        elif s["occupied"]:
            bucket["occupied"] += 1
        else:
            bucket["free"] += 1
    site = {"free": 0, "occupied": 0, "unknown": 0, "total": 0}
    for level, b in per_level.items():
        client.publish(f"{PREFIX}/parking/{SITE}/{level}/summary", json.dumps(b), qos=1, retain=True)
        for k in site:
            site[k] += b[k]
    client.publish(f"{PREFIX}/parking/{SITE}/summary", json.dumps(site), qos=1, retain=True)

def on_connect(client, userdata, flags, reason_code, properties):
    client.subscribe(f"{PREFIX}/parking/{SITE}/+/+/state", qos=1)

def on_message(client, userdata, msg):
    parts = msg.topic.split("/")
    level, bay = parts[-3], parts[-2]
    try:
        data = json.loads(msg.payload)
        bays[(level, bay)] = {"occupied": bool(data["occupied"]), "ts": float(data.get("ts", time.time()))}
    except (ValueError, KeyError):
        return
    publish_summaries(client)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="parking-agg-p3v6z8")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_forever()

Simulate a few bays from the terminal:

P=connectmq/demo/p3v6z8/parking/central
mosquitto_pub -h broker.connectmq.com -p 1883 -i sim-p3v6z8 -r -q 1 -t $P/l1/b001/state -m "{\"occupied\":true,\"ts\":$(date +%s)}"
mosquitto_pub -h broker.connectmq.com -p 1883 -i sim-p3v6z8 -r -q 1 -t $P/l1/b002/state -m "{\"occupied\":false,\"ts\":$(date +%s)}"
mosquitto_sub -h broker.connectmq.com -p 1883 -i view-p3v6z8 -t "$P/+/summary" -t "$P/summary" -v

Note that the demo prefix adds levels: the bay state topic above is eight levels deep, the maximum the public broker allows. On a private broker you would drop the prefix and start at parking/.

For a large deployment, publishing every summary on every change is wasteful. Batch updates every second or two, and if one aggregator is not enough, MQTT 5 shared subscriptions on a paid plan let several instances split the load.

The Live Availability Map

The browser connects over secure WebSocket and subscribes only to summaries, or to bay states for a detailed level view. With MQTT.js:

import mqtt from "mqtt";

const PREFIX = "connectmq/demo/p3v6z8/parking/central";
const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
  clientId: "parking-map-" + Math.random().toString(16).slice(2, 8),
  protocolVersion: 5,
});

client.on("connect", () => {
  client.subscribe(`${PREFIX}/+/summary`, { qos: 1 });
  client.subscribe(`${PREFIX}/l1/+/state`, { qos: 0 });
});

client.on("message", (topic, payload) => {
  const data = JSON.parse(payload.toString());
  const parts = topic.split("/");
  if (topic.endsWith("/summary")) {
    const level = parts[parts.length - 2];
    const el = document.querySelector(`[data-level="${level}"] .free`);
    if (el) el.textContent = `${data.free} free of ${data.total}`;
  } else {
    const bay = parts[parts.length - 2];
    const cell = document.querySelector(`[data-bay="${bay}"]`);
    if (cell) cell.className = data.occupied ? "bay occupied" : "bay free";
  }
});

On a map library such as Leaflet, the same handler would recolour a marker per site. Because summaries are retained, the map paints the current picture the instant the page loads. Our guide to MQTT over WebSocket covers browser connection details and credential handling, which matters: a public web page must never embed credentials that can publish to bay topics.

Deployment Checklist

  1. Pick sensors per environment: pucks outdoors, ultrasonic indoors, cameras where privacy rules allow.
  2. Bridge LoRaWAN or wired networks through gateways to keep connection counts low.
  3. Retain bay states and summaries; include timestamps.
  4. Treat silent sensors as unknown, and alert on low battery.
  5. Never ship publish-capable credentials to public map clients; serve summaries through your own back end instead.
  6. Use TLS on port 8883 for gateways and 8084 for browsers.
  7. Log state changes to a database with a separate subscriber for occupancy analytics.

Common Pitfalls in Parking Deployments

Adjacent-bay interference. A large van can disturb the magnetometer in the next bay. Calibrate sensors after installation and prefer combined magnetometer and radar units in tight layouts.

Ghost occupancy. Snow, standing water or a dropped metal object can make a bay look occupied for days. Flag bays that have not changed state for an unusually long time and review them.

Clock drift. If timestamps come from sensors, a drifting clock makes fresh data look stale. Let the gateway or network server stamp messages on arrival.

Over-publishing summaries. Hundreds of signs and apps receiving a summary on every single bay change wastes bandwidth. Rate-limit summaries and only publish when the numbers actually change.

Scaling Beyond a Single Car Park

Once several sites publish under parking/<site>/..., a city-wide guidance sign subscribes to parking/+/summary and nothing else. This is the same messaging pattern used in the broader smart city IoT architecture, where parking, lighting and air quality share one broker but stay in separate subtrees. Temperature-sensitive logistics uses a similar fleet pattern, as described in cold chain monitoring.

Running the broker yourself means patching, certificates and monitoring for a system drivers rely on every day. A managed MQTT broker like ConnectMQ handles TLS, WebSocket endpoints and connection management, while the live topic explorer in the portal lets you watch bay states arrive. If you are comparing hosting models, our overview of the cloud MQTT broker approach helps, and the IoT automation guide collects related projects.

Start Your Smart Parking Pilot on ConnectMQ

Pilot one level with a handful of sensors and a gateway, publish to ConnectMQ, and put the live map on a test screen. Create a free Serverless broker at app.connectmq.com, no credit card required, and move to a paid or dedicated plan when the bay count grows.

Frequently Asked Questions

How many MQTT connections does a smart parking system need?

Usually far fewer than the number of bays. LoRaWAN network servers and indoor gateways publish for many sensors over a single connection, so connection count scales with gateways and apps, not bays.

Which sensor is best for outdoor parking bays?

Battery-powered magnetometer pucks, ideally combined with radar to reduce false readings, are the common choice outdoors because they survive weather and last years on a battery.

How do I keep the public map from being abused?

Proxy the data through your own back end, for example a small server that subscribes to summaries and pushes them to the page. Never embed credentials in a public page that could publish to bay or summary topics.

Can I use cameras instead of bay sensors?

Yes. An edge device runs detection and publishes only per-bay occupancy, never video, which keeps bandwidth low and helps with privacy compliance.

Related articles

Zigbee2MQTT Setup Guide: From Coordinator to Dashboard

Install Zigbee2MQTT with Docker, configure the coordinator and MQTT connection, pair and rename devices, control them over MQTT and view their states on a dashboard, testing first on a free public broker.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.