SensorRig

DHT11 After 18 Months of Continuous Logging: An Honest Accuracy Review

7 August 2026 · 9 min read

Hardware used in this build

  • DHT11 — Indoor temp + humidity (long-run logging)
    Fine for 20-80 %RH yes/no questions; out of spec below 20 %RH. No standalone product page yet.
  • Raspberry Pi 4 — 1-Wire master + villa collector
    Used in the running shed build; works reliably.
  • ESP32-C3 dev board — Shed node MCU
    The shed node MCU. Runs the AI_optimized_shed_TX.ino sketch. Seeed's own board.
  • HC-12 433 MHz wireless module — Radio link shed <-> Pi 4
    HiLetgo HC-12 433 MHz SI4438 module with antenna. Reliable for 100–1 000 m links.

Full parts list & affiliate disclosure →

One DHT11 in my house has been logging on a one-minute interval into the same SQLite database as everything else since 25 January 2025. As of this morning that is 558 distinct days and 26,318 rows in my rollup table. A note on that table: despite being named measurements_1h it actually stores half-hour buckets — every timestamp lands on :00 or :30 — so 26,318 rows is roughly 13,160 hours, not 26,318 hours. I only noticed while writing this up, which is its own small lesson about trusting a table name. The question I actually needed answered was not “is the DHT11 accurate?” — the datasheet answers that — but “over 18 months in a real, unconditioned room, does this part ever tell me something I can act on?”

The short version: 10,090 of those 26,318 buckets, 38.3%, have a mean humidity below 20 %RH, which is outside the range the part is specified for. It reports those numbers with exactly the same confidence as the in-spec ones. That, not the ±5 %RH accuracy figure, is the real story.

This is not a bench review. I have no climate chamber and no reference hygrometer worth the name. What I have is 18 months of one-minute data and a willingness to say which parts of it are junk.

What the datasheet actually promises

Worth being precise, because most “DHT11 is garbage” posts are arguing against a spec the part never claimed to meet.

ParameterDHT11 (datasheet)DHT22/AM2302 (datasheet)
Humidity range20–80 %RH0–100 %RH
Humidity accuracy±5 %RH±2 %RH (typ.)
Humidity resolution1 %RH0.1 %RH
Temperature range0–50 °C−40–80 °C
Temperature accuracy±2 °C±0.5 °C
Min sampling interval1 s2 s

I run DHT11 only. The DHT22 column is a datasheet figure quoted for context — I do not own DHT22s and will not pretend otherwise.

Two things the table does not tell you, both of which I can confirm from my own logged values:

My units report 0.1 °C steps, not 1 °C. Over the last 14 days of raw one-minute rows (19,572 samples) every temperature value is an exact multiple of 0.1, spanning 26.9–35.0 °C, with 83 distinct values. Every humidity value is an integer. So the temperature channel is finer than the classic 1 °C-resolution datasheet table implies, while humidity really is 1 %RH. If your DHT11 emits integer degrees, you have a different firmware revision than mine — check your own data before assuming.

Out-of-range is not graceful. The Mouser-hosted DHT11 datasheet warns that operating the part outside its stated working range “can result in 3%RH signal shift/discrepancy”. In my data the failure is far uglier than a 3 % shift. Below is the monthly mean humidity for the full 18 months:

Monthmean %RHMonthmean %RH
2025-012.42025-1127.0
2025-023.92025-1211.2
2025-0314.52026-014.8
2025-0420.52026-021.5
2025-0531.02026-035.6
2025-0641.22026-0414.1
2025-0746.72026-0527.5
2025-0844.52026-0636.6
2025-0946.12026-0740.8
2025-1038.52026-08 (partial)45.4

A monthly mean of 1.5 %RH in February 2026 is not a measurement. Heated indoor air in a cold climate does get genuinely dry — single-digit RH is possible — but 276 buckets (1.0% of the record) came back with a mean under 1 %RH, and the lowest bucket means are on the order of 0.001 %RH. That is the sensor falling off the bottom of its transfer function, not a room. 17.2% of all buckets (4,538) are below 5 %RH.

The seasonal shape is real: the curve is smooth, repeats across two winters, and tracks the heating season. What is not real is the magnitude at the low end. I trust the direction below 20 %RH and nothing else.

The top end never came up: zero buckets above 80 %RH in the entire record, and zero temperature buckets outside 0–50 °C. So in my installation only one of the two range limits ever bites.

The cross-check I wanted, and why I could not run it

The plan was to difference the DHT11’s temperature channel against a co-located DS18B20 and watch the offset for drift over 18 months. A stable offset means the DHT11 has not walked; a trending offset means something has, and the DS18B20 (±0.5 °C over −10 to +85 °C, per its datasheet) is four times tighter, so the suspect would be obvious.

I could not run it, and the reason is worth stating rather than hiding: none of my eight DS18B20s are co-located with that DHT11. They are all on the heating system. Differencing anyway gives mean offsets from −3.1 to +7.0 °C with standard deviations of 5.9 to 13.4 °C — those numbers describe the distance between a room and a heating pipe, not sensor drift. Publishing them as a drift figure would be dishonest, so here they are as a negative result instead.

The query is still the right one once the hardware is right:

SELECT strftime('%Y-%m', ts, 'unixepoch', 'localtime') AS month,
       COUNT(*)                            AS n,
       ROUND(AVG(d.avg - w.avg), 3)        AS mean_offset_c,
       ROUND(MIN(d.avg - w.avg), 2)        AS min_off,
       ROUND(MAX(d.avg - w.avg), 2)        AS max_off
FROM measurements_1h d
JOIN measurements_1h w ON w.ts = d.ts AND w.source = 'heat' AND w.metric = :ref
WHERE d.source = 'basement' AND d.metric = 'temp' AND d.avg > 0
GROUP BY month ORDER BY month;

Averaging does real work here. A single sample quantised to 1 %RH is coarse, but the mean of thousands of samples straddling a quantisation boundary recovers sub-step information — provided the underlying signal actually wanders across that boundary. Indoors it does, constantly. That is dithering, and it is free.

The fix is a hardware one, and it is on my list: mount a spare DS18B20 within a few centimetres of the DHT11, leave it a full month, then run the query above. The useful output is the trend in mean_offset_c from month to month, not its absolute value — the absolute value is just the thermal gradient between two points a few centimetres apart, and it will not be zero.

Reading it reliably on a Pi at all

Before accuracy, reliability. The DHT11’s single-wire protocol is timing-critical and Linux is not a real-time OS. Bit-banging it from userspace means the scheduler will occasionally preempt you mid-frame and you get a checksum failure. That is a broken sampling strategy, not a broken sensor.

My collector config sets max_retries = 15 on that channel — a number I arrived at by raising it until the failure rate stopped bothering me, not by measurement. It works: over the 14-day raw window, exactly 3 of 19,572 temperature samples and 3 of 19,572 humidity samples were logged as 0 after all retries were exhausted. On the full rollup record, 131 buckets have a mean humidity of exactly 0. So the retry loop is doing its job, and the residual failures are rare enough to filter downstream.

async def read_dht11(pin, retries=15, gap=2.1):
    """DHT11 needs >=1s between reads; 2.1s is a lazy safe margin."""
    for attempt in range(retries):
        try:
            # note the order: this driver hands back (humidity, temp)
            h, t = await asyncio.to_thread(dht_read, pin)
            if h is None or t is None:
                raise ValueError("bad frame")
            if not (0 <= h <= 100) or not (-10 <= t <= 60):
                raise ValueError(f"implausible: {t}C {h}%")
            return t, h  # ...and the collector wants (temp, humidity)
        except (ValueError, RuntimeError):
            if attempt == retries - 1:
                return None, None
            await asyncio.sleep(gap)

Two mistakes visible in my own data. First, that range gate passes 0.0, so exhausted reads got persisted as a plausible-looking zero instead of a NULL — which is why I have to write AND avg > 0 in every query forever. Return None and store NULL; do not let a sentinel value into a numeric column. (The logger underneath is SQLite in WAL mode, and the to_thread wrapper above is the same asyncio collector pattern I use for every bus.) Second, the better fix is architectural: my ESP32-C3 nodes read their sensors locally and ship validated frames over HC-12 433 MHz serial, so the microcontroller owns the timing and the Pi never sees a half-frame.

What went wrong

Zero as a sentinel. Covered above. 131 poisoned buckets and a permanent avg > 0 tax on every query. Cheapest possible mistake to avoid, and I made it anyway.

Self-heating. A DHT11 mounted close to a Pi reads warm. This is physics, not a fault — the board is a heat source and the sensor measures the air it is in. It biases both channels, because warming a parcel of air lowers its RH at constant absolute humidity. Everything I mount now is on a flying lead away from the board.

Winter dry-out below spec. 38.3% of my buckets are out of range and the part never says so. I now flag anything under 20 %RH rather than trusting it, and discard anything under 1 %RH outright.

Frozen output. A sensor that returns the same valid, plausible number forever looks exactly like a very stable room. On the raw 14-day window the longest identical run is 37 consecutive temperature samples and 107 consecutive humidity samples — about 37 and 107 minutes at a one-minute cadence. That is normal for a slow-moving room with 1 %RH steps, and it also tells me where to set the alarm: a staleness threshold below ~2 hours would fire on healthy data. Set it from your own run-length distribution, not from a guess.

Would I use one again

For “is this room damp” and “did the humidifier run”, yes. It is cheap, it is still working after 18 months, and ±5 %RH is fine for a yes/no question in the 20–80 %RH band.

For anything outside that band, no — and in my house that is 38% of the year. The DHT11’s problem is not that it is inaccurate. It is that it is coarse, range-limited, and silent about both. A known offset you can calibrate out. A sensor that quietly leaves its own spec for four months a winter, you cannot.

For temperature alone I reach for a DS18B20 instead — it reports its own bad reads via CRC, which is a different and much better failure mode; see what happened when mine threw CRC errors on a short cable.

Related reading