A Raspberry Pi RFID MQTT logger records every badge scan at a door, workshop machine or tool cabinet and sends it to any system that needs to know: a log database, a dashboard or an alerting script. In this tutorial you will wire an RC522 reader, read tags in Python, decide locally whether a tag is allowed, and publish structured access events over MQTT, including alerts for unknown cards.
What You Will Build
The Pi runs one Python service that:
- Polls the RC522 reader for 13.56 MHz MIFARE cards and key fobs.
- Hashes each tag's UID so the raw ID never leaves the device.
- Looks the hash up in a local allowlist file and decides granted or denied.
- Publishes an access event for every scan and a separate alert for unknown tags.
- Keeps a retained "last access" message and an online/offline status.
The access decision is made on the Pi, not in the cloud. That way the door keeps working when the network is down; MQTT is used for logging and notification, which is exactly what it is good at.
Wiring the RC522
The RC522 module talks SPI and runs on 3.3 V only. Connect it like this:
| RC522 pin | Raspberry Pi physical pin | Function |
|---|---|---|
| SDA (SS) | 24 | SPI CE0 |
| SCK | 23 | SPI clock |
| MOSI | 19 | SPI MOSI |
| MISO | 21 | SPI MISO |
| IRQ | Not connected | |
| GND | 6 | Ground |
| RST | 22 | GPIO25 |
| 3.3V | 1 | Power |
Never connect the module to 5 V. Enable SPI with sudo raspi-config nonint do_spi 0 and reboot; afterwards ls /dev/spidev* should list /dev/spidev0.0.
Installing the Libraries
sudo apt update
sudo apt install -y python3-venv python3-dev
mkdir -p ~/rfid-mqtt && cd ~/rfid-mqtt
python3 -m venv .venv
source .venv/bin/activate
pip install "paho-mqtt>=2.0" mfrc522 spidev
The mfrc522 package depends on RPi.GPIO, which does not work on the Raspberry Pi 5. On a Pi 5, replace it with the compatible rpi-lgpio package inside the virtual environment:
pip uninstall -y RPi.GPIO
pip install rpi-lgpio
Test the reader before adding MQTT:
python -c "from mfrc522 import SimpleMFRC522; r = SimpleMFRC522(); print('Hold a tag near the reader'); print(r.read_id())"
Hold a card or fob against the antenna and a long number should be printed.
Designing the Events
| Topic | Payload | QoS | Retained |
|---|---|---|---|
connectmq/demo/2fa7c1/door1/access |
Access event JSON | 1 | No |
connectmq/demo/2fa7c1/door1/alert |
Unknown tag JSON | 1 | No |
connectmq/demo/2fa7c1/door1/last |
Most recent event | 1 | Yes |
connectmq/demo/2fa7c1/door1/status |
online / offline |
1 | Yes |
An access event looks like this:
{
"reader": "door1",
"tag": "a3f09b7c41de2280",
"name": "Alice",
"granted": true,
"ts": 1792163300
}
The tag field is a keyed hash of the UID, not the UID itself. On a public broker, and in log files generally, a raw UID is something you should not spread around, because low-cost cards can be cloned from it.
The Allowlist File
Create allowlist.json in the project folder. You will fill in hashes after the first scans:
{
"a3f09b7c41de2280": "Alice",
"51c2e8d4097fab13": "Workshop spare fob"
}
To enrol a new card, scan it once, copy the tag value from the alert message, add it to the file with a name, and send the service a SIGHUP to reload, or simply restart it.
The Raspberry Pi RFID MQTT Script
Save as rfid_mqtt.py:
#!/usr/bin/env python3
"""RC522 access logging over MQTT."""
import hashlib
import hmac
import json
import os
import signal
import time
import uuid
from pathlib import Path
import paho.mqtt.client as mqtt
import RPi.GPIO as GPIO
from mfrc522 import SimpleMFRC522
BROKER = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME")
PASSWORD = os.getenv("MQTT_PASSWORD")
CLIENT_ID = os.getenv("MQTT_CLIENT_ID", f"rfid-{uuid.uuid4().hex[:8]}")
PREFIX = os.getenv("MQTT_PREFIX", "connectmq/demo/2fa7c1/door1")
READER_NAME = os.getenv("READER_NAME", "door1")
SECRET = os.getenv("TAG_SECRET", "change-me-to-a-long-random-string").encode()
ALLOWLIST_FILE = Path(os.getenv("ALLOWLIST", "allowlist.json"))
DEBOUNCE_S = 3
reader = SimpleMFRC522()
allowlist = {}
def load_allowlist(*_):
global allowlist
try:
allowlist = json.loads(ALLOWLIST_FILE.read_text())
print(f"Loaded {len(allowlist)} allowed tags")
except (OSError, json.JSONDecodeError) as err:
print(f"Could not load allowlist: {err}")
def tag_hash(uid: int) -> str:
digest = hmac.new(SECRET, str(uid).encode(), hashlib.sha256).hexdigest()
return digest[:16]
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"Connection refused: {reason_code}")
return
print(f"Connected as {CLIENT_ID}")
client.publish(f"{PREFIX}/status", "online", qos=1, retain=True)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
client.on_connect = on_connect
client.will_set(f"{PREFIX}/status", "offline", qos=1, retain=True)
client.reconnect_delay_set(min_delay=1, max_delay=60)
client.max_queued_messages_set(500)
if USERNAME:
client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
client.tls_set()
load_allowlist()
signal.signal(signal.SIGHUP, load_allowlist)
client.connect(BROKER, PORT, keepalive=30)
client.loop_start()
last_tag, last_time = None, 0.0
try:
while True:
uid = reader.read_id_no_block()
if uid is None:
time.sleep(0.1)
continue
tag = tag_hash(uid)
now = time.time()
if tag == last_tag and now - last_time < DEBOUNCE_S:
continue # card is still resting on the reader
last_tag, last_time = tag, now
name = allowlist.get(tag)
event = {
"reader": READER_NAME,
"tag": tag,
"name": name,
"granted": name is not None,
"ts": int(now),
}
payload = json.dumps(event)
client.publish(f"{PREFIX}/access", payload, qos=1)
client.publish(f"{PREFIX}/last", payload, qos=1, retain=True)
if name is None:
client.publish(f"{PREFIX}/alert", payload, qos=1)
print(event)
# Drive a door strike or LED here with GPIO.output() if granted.
except KeyboardInterrupt:
pass
finally:
client.publish(f"{PREFIX}/status", "offline", qos=1, retain=True).wait_for_publish(2)
client.loop_stop()
client.disconnect()
GPIO.cleanup()
The debounce stops a card lying on the reader from generating an event every tenth of a second. Because paho queues QoS 1 messages in memory while the connection is down, scans during a short network outage are delivered after reconnect; max_queued_messages_set caps that queue so a long outage cannot exhaust memory.
Testing the Flow
Watch everything from another terminal:
mosquitto_sub -h broker.connectmq.com -p 1883 -i audit-2fa7c1 \
-t 'connectmq/demo/2fa7c1/door1/#' -v
Scan an unknown card: you will see an access event with "granted": false and the same payload on alert. Add its hash to allowlist.json, send the process a SIGHUP with kill -HUP <pid>, and scan again to see "granted": true with the name.
Logging and Alerting
The reader only publishes; persistence and notification are handled by other MQTT clients, which keeps the device simple. Common choices are:
- A Python subscriber that writes every
accessevent into SQLite, as shown in saving MQTT data to SQLite with Python. - An email or chat notification on the
alerttopic using the pattern in MQTT email alerts with Python. - A Node-RED flow that displays the retained
lastmessage on a dashboard. - A door strike or indicator light driven by a second Pi, using the Raspberry Pi relay MQTT controller.
Beyond the Front Door
The same reader and event format fit several other jobs. In a makerspace, a reader next to a laser cutter or lathe can log who used the machine and for how long: publish a second event when the card is removed, and a subscriber can compute session length. For attendance in a classroom or small office, the retained last topic doubles as a live "who just arrived" display, and the SQLite log becomes a timesheet. In a tool cabinet, a scan can unlock a drawer and record which fob opened it.
In each case, only the reader name and topic prefix change. Keep one prefix level per location, such as site1/workshop/lathe, and a single subscriber with a + wildcard can collect events from every reader. A granted field and a stable hashed tag are all the downstream tools need, which keeps the device code identical across the whole building.
Feedback for the Person Scanning
People expect to see or hear that a scan worked. Add a green and a red LED, or a small buzzer, on spare GPIO pins and drive them with GPIO.output() right after the decision; note that the reader library already puts RPi.GPIO in BOARD numbering mode, so use physical pin numbers. Give feedback locally rather than waiting for an MQTT round trip, so it stays instant even on a slow network.
Security Considerations
RFID projects touch physical security, so be realistic about their limits. Basic MIFARE Classic cards and many fobs can be cloned, so a UID-based system is suitable for logging, attendance and convenience access, not for protecting high-value areas. Beyond that:
- Never run an access system on the public test broker. Everything at broker.connectmq.com is readable by anyone and is meant only for the first test.
- Use a private broker with TLS and per-device credentials, so nobody can publish fake events or read your logs.
- Keep
TAG_SECRETout of the code and out of version control; store it in a systemd environment file withchmod 600. - Make the grant decision locally so a network outage does not lock people out or open the door.
For broader guidance, read our MQTT security best practices.
Moving to Production
A hosted MQTT broker such as ConnectMQ provides TLS on port 8883, generated credentials bound to each client ID, and a portal where you can watch access events arrive in the live topic explorer. Set the environment variables in your service file:
MQTT_HOST=broker.connectmq.com
MQTT_PORT=8883
MQTT_USERNAME=YOUR_USERNAME
MQTT_PASSWORD=YOUR_PASSWORD
MQTT_CLIENT_ID=YOUR_CLIENT_ID
MQTT_PREFIX=site1/building-a/door1
TAG_SECRET=a-long-random-secret
The Raspberry Pi MQTT tutorial includes a systemd service template you can adapt so the reader starts at boot.
Get Started with ConnectMQ
Give your access logs a private, encrypted path. Create a free Serverless broker on ConnectMQ, no credit card required, and connect your reader with the generated credentials over TLS. It is a cloud MQTT broker that your logger, dashboard and alerting scripts can all reach without opening ports at the site.
Frequently Asked Questions
Which RFID cards work with the RC522?
The RC522 reads 13.56 MHz ISO 14443A cards such as MIFARE Classic, MIFARE Ultralight and many key fobs. It does not read 125 kHz proximity cards.
Why hash the RFID UID before publishing it?
A raw UID can be used to clone simple cards, and MQTT messages often end up in logs. A keyed hash still identifies the card uniquely without revealing the UID.
Does the RC522 work on a Raspberry Pi 5?
Yes, with SPI enabled and the rpi-lgpio package installed in place of RPi.GPIO, which the mfrc522 library expects.
What happens to scans when the network is down?
The Pi still decides access locally. QoS 1 events are queued in memory and delivered after reconnect, as long as the service keeps running.



