Cold storage temperature monitoring protects food, ingredients, pharmaceuticals and samples that can be lost in a few hours when a freezer or cold room fails. This use case shows how to put MQTT sensors into freezers and cold rooms, distinguish real temperature excursions from harmless door openings, detect sensors that go offline and keep an excursion log you can show an auditor.
What Can Go Wrong in Cold Storage
Most cold storage losses do not come from dramatic breakdowns. They come from ordinary events that nobody notices in time:
- A door left ajar after a night delivery.
- A compressor that trips on a hot afternoon and does not restart.
- An evaporator that ices up, so the room warms slowly over hours.
- A power cut over the weekend.
- A defrost cycle that runs too long after a controller change.
A manual temperature log, filled in twice per shift, catches none of these between readings. Continuous monitoring with alerts closes that gap. The same principles apply to refrigerated transport, covered in the cold chain monitoring article; this one focuses on fixed freezers and cold rooms in a plant, warehouse or kitchen.
The Setup
| Component | Role | Example choice |
|---|---|---|
| Temperature probe | Measures air or product temperature | Waterproof DS18B20 on a flat cable through the door seal |
| Door contact | Explains short temperature rises | Reed switch on the door frame |
| Sensor node | Reads probe and door, publishes | ESP32 mounted outside the freezer on mains power |
| MQTT broker | Receives readings and alerts | ConnectMQ managed broker |
| Alert logic | Excursion and offline detection | Portal threshold rules plus a Python service |
| Excursion log | Evidence for audits and claims | Database written by the alert service |
Keep the ESP32 outside the cold space. Electronics and batteries struggle at freezer temperatures, and Wi-Fi signals do not pass through the metal walls of a cold room. Only the probe cable goes inside.
Where to put the probe
Air temperature near the evaporator changes fast and exaggerates every door opening. Product temperature changes slowly and reflects what actually matters. A common compromise is to place the probe in the warmest part of the room, away from the evaporator outlet, and optionally inside a small bottle of glycol or a product simulant that buffers it, so readings follow product temperature rather than air gusts. Map the room once with a few probes to find the warm spot before choosing the permanent position.
Sensor Firmware
The sketch below publishes temperature and door state every 30 seconds, and immediately when the door changes. It sets a Last Will message, so the broker announces the sensor as offline if it disappears. Install PubSubClient, OneWire and DallasTemperature. For sensor wiring details, see the ESP32 DS18B20 MQTT tutorial.
#include <WiFi.h>
#include <PubSubClient.h>
#include <OneWire.h>
#include <DallasTemperature.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* CLIENT_ID = "freezer2-6c0f3b";
const char* BASE = "connectmq/demo/6c0f3b/warehouse/freezer2";
const int PROBE_PIN = 4;
const int DOOR_PIN = 27; // reed switch to GND, closed door = LOW
const uint32_t PERIOD_MS = 30000;
OneWire oneWire(PROBE_PIN);
DallasTemperature probe(&oneWire);
WiFiClient net;
PubSubClient mqtt(net);
char topicTelemetry[96], topicStatus[96];
int lastDoor = -1;
uint32_t lastPub = 0;
void connectAll() {
if (WiFi.status() != WL_CONNECTED) {
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(250);
}
while (!mqtt.connected()) {
if (mqtt.connect(CLIENT_ID, topicStatus, 1, true, "offline")) {
mqtt.publish(topicStatus, "online", true);
} else {
delay(3000);
}
}
}
void publishReading(bool doorOpen) {
probe.requestTemperatures();
float t = probe.getTempCByIndex(0);
char payload[128];
if (t == DEVICE_DISCONNECTED_C) {
snprintf(payload, sizeof(payload), "{\"temp_c\":null,\"door_open\":%s,\"fault\":\"probe\"}",
doorOpen ? "true" : "false");
} else {
snprintf(payload, sizeof(payload), "{\"temp_c\":%.2f,\"door_open\":%s,\"rssi\":%d}",
t, doorOpen ? "true" : "false", WiFi.RSSI());
}
mqtt.publish(topicTelemetry, payload, true);
}
void setup() {
pinMode(DOOR_PIN, INPUT_PULLUP);
snprintf(topicTelemetry, sizeof(topicTelemetry), "%s/telemetry", BASE);
snprintf(topicStatus, sizeof(topicStatus), "%s/status", BASE);
probe.begin();
WiFi.mode(WIFI_STA);
mqtt.setServer("broker.connectmq.com", 1883);
connectAll();
}
void loop() {
connectAll();
mqtt.loop();
int door = digitalRead(DOOR_PIN) == HIGH; // HIGH = magnet away = open
if (door != lastDoor || millis() - lastPub >= PERIOD_MS) {
publishReading(door);
lastDoor = door;
lastPub = millis();
}
delay(200);
}
The Last Will is the safety net that a manual log never had: if the node loses power or Wi-Fi, the broker publishes offline on its status topic after the keepalive timeout. The ESP32 MQTT Last Will guide explains the mechanism.
Excursion Logic Without Nuisance Alarms
The naive approach, alarm whenever temperature exceeds the limit, fails within a day. Every delivery opens the door, air temperature spikes, and staff learn to ignore the alarm. Good cold storage temperature monitoring uses a few rules together:
- Limit plus delay. Raise an excursion only if the temperature stays above the limit for a set time, for example 15 minutes for a freezer.
- Door-aware delay. While the door is open, allow a longer grace period, but raise a separate door-open alert if it stays open too long.
- Hard limit. A much higher temperature triggers immediately, regardless of delay.
- Offline and probe faults. A silent sensor or a disconnected probe is an alert in its own right.
- Excursion record. Each excursion gets a start time, end time, peak temperature and duration.
The limits themselves come from your products and your food safety or quality plan. Use those documents rather than generic numbers.
Excursion service in Python
import json
import sqlite3
import time
import paho.mqtt.client as mqtt
PREFIX = "connectmq/demo/6c0f3b/warehouse"
LIMIT_C = -15.0 # from your product requirements
HARD_LIMIT_C = -5.0
DELAY_S = 15 * 60
DOOR_MAX_S = 5 * 60
STALE_S = 3 * 60
db = sqlite3.connect("excursions.db", check_same_thread=False)
db.execute("""CREATE TABLE IF NOT EXISTS excursions (
unit TEXT, start_ts INTEGER, end_ts INTEGER, peak_c REAL, reason TEXT)""")
units = {}
def alert(client, unit, kind, detail):
msg = {"ts": int(time.time()), "unit": unit, "kind": kind, **detail}
client.publish(f"{PREFIX}/{unit}/alert", json.dumps(msg), qos=1, retain=True)
print("ALERT", msg)
def on_connect(client, userdata, flags, reason_code, properties):
client.subscribe(f"{PREFIX}/+/telemetry", qos=1)
client.subscribe(f"{PREFIX}/+/status", qos=1)
def on_message(client, userdata, msg):
unit = msg.topic.split("/")[4]
now = time.time()
u = units.setdefault(unit, {"above_since": None, "door_since": None,
"excursion": None, "seen": now})
if msg.topic.endswith("/status"):
if msg.payload == b"offline":
alert(client, unit, "offline", {})
return
d = json.loads(msg.payload)
u["seen"] = now
if d.get("temp_c") is None:
alert(client, unit, "probe_fault", {})
return
t = d["temp_c"]
u["door_since"] = (u["door_since"] or now) if d["door_open"] else None
if u["door_since"] and now - u["door_since"] > DOOR_MAX_S:
alert(client, unit, "door_open_too_long", {"seconds": int(now - u["door_since"])})
u["door_since"] = now
if t > LIMIT_C:
u["above_since"] = u["above_since"] or now
delay = DELAY_S * (2 if d["door_open"] else 1)
exc = u["excursion"]
if exc:
exc["peak"] = max(exc["peak"], t)
elif t > HARD_LIMIT_C or now - u["above_since"] > delay:
u["excursion"] = {"start": u["above_since"], "peak": t}
alert(client, unit, "excursion_start", {"temp_c": t, "limit_c": LIMIT_C})
else:
exc = u["excursion"]
if exc:
db.execute("INSERT INTO excursions VALUES (?,?,?,?,?)",
(unit, int(exc["start"]), int(now), exc["peak"], "above_limit"))
db.commit()
alert(client, unit, "excursion_end",
{"duration_min": round((now - exc["start"]) / 60, 1), "peak_c": exc["peak"]})
u["above_since"] = None
u["excursion"] = None
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="cold-alerts-6c0f3b")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_start()
while True:
time.sleep(30)
for unit, u in units.items():
if time.time() - u["seen"] > STALE_S:
alert(client, unit, "stale", {"seconds_silent": int(time.time() - u["seen"])})
u["seen"] = time.time()
The excursion start time is recorded as the moment the temperature first crossed the limit, not when the delay expired, so the log shows the true duration. For the simplest cases you can also create threshold alert rules on the temp_c field directly in the ConnectMQ portal and review every crossing in its alerts log, keeping the Python service for the delay and door logic.
Getting Alerts to People
The service publishes alerts to MQTT; something still has to reach a phone at 3 a.m. Because ConnectMQ does not send emails or SMS itself, the honest pattern is an external tool that subscribes to the alert topics: a Node-RED flow with an email or messaging node, Home Assistant notifications for smaller sites, or a few extra lines in the Python service that call your notification provider. Define an escalation path: first the shift lead, then the maintenance on-call, then the site manager if nobody acknowledges.
Handling Defrost Cycles
Automatic defrost is the other frequent source of false alarms. During defrost the evaporator heats up and air temperature near it rises for a short time, which is normal. Three measures keep it from triggering excursions. First, keep the probe away from the evaporator, as described above. Second, make sure the excursion delay is longer than a normal defrost period on that unit, which you can see clearly in a day of recorded data. Third, if the refrigeration controller exposes a defrost signal, publish it alongside the temperature and extend the delay while defrost is active, exactly as the service does for door openings. A defrost that runs much longer than usual is itself worth an alert, because it often points to a failing heater, sensor or controller setting.
Rollout Checklist
- Map each room with several probes for a day to find the warm spot.
- Install one node per unit, electronics outside, probe at the warm spot.
- Set limits and delays from your product requirements and quality plan.
- Test the alarms: open the door, unplug a node, disconnect a probe, and confirm each alert arrives.
- Calibrate probes against a reference thermometer and record the check.
- Review the excursion log weekly and act on repeat offenders, such as a door that is often left open.
- Move to a private broker with credentials from the portal and TLS on port 8883 before going live.
From Demo to Production
The tutorial runs on the public MQTT test broker, where all topics are visible to anyone, which is fine for testing but not for operational data. For production, a cloud MQTT broker gives every freezer node an encrypted, authenticated connection and lets the alert service, dashboards and the quality team subscribe from anywhere. Temperature records also complement IoT quality control in manufacturing and batch manufacturing traceability, since excursion windows can be linked to the lots stored at the time. The smart manufacturing guide puts all of this into a plant-wide architecture.
Monitor Your Cold Storage on ConnectMQ
ConnectMQ provides a private MQTT broker with TLS, Last Will support, retained messages, a live dashboard and threshold alert rules in the portal. Create a broker on the free Serverless plan at app.connectmq.com, no credit card required, and connect your first freezer today.
Frequently Asked Questions
How often should cold storage temperature be recorded?
Every 30 seconds to 5 minutes is typical for continuous monitoring. Faster reporting gives quicker alerts, while the excursion delay prevents nuisance alarms.
How do I avoid alarms every time the door opens?
Require the temperature to stay above the limit for a delay period before alerting, extend that delay while the door is open, and alert separately if the door stays open too long.
What happens if a sensor loses power?
With a Last Will message, the broker publishes an offline status after the keepalive timeout, and the alert service treats it as an alarm.
Should the probe measure air or product temperature?
Product temperature matters most. Placing the probe in a buffer such as a glycol bottle at the warmest spot gives readings that follow product temperature and ignore brief air spikes.



