TL;DR:
Most DHT problems are caused by timing and usage errors, not bad sensors.
DHT uses a fragile pulse-based protocol, and libraries must actively protect users from common mistakes.
I made a short video showing the most common DHT mistakes and how to fix them:
(It covers the exact issues described below in a quick, practical way.)
Hi everyone,
DHT11 and DHT22 are among the most widely used Arduino sensors —
and at the same time among those most often considered unreliable.
The most common symptoms people report:
- occasional NaN values
- “random” value spikes
- code that works for a while and then suddenly stops
- MCU resets used as a “solution”
In most cases, the sensor is not the problem.
The problem is how DHT sensors are used — and the fact that most libraries do not protect users from common mistakes.
To understand why this happens, we first need to understand how DHT actually works.
DHT is not a “digital sensor” in the usual sense
Even though it uses a single data pin, DHT is not a typical digital sensor that simply sends 0s and 1s.
DHT uses a bit-banging protocol based entirely on pulse timing.
A single communication cycle looks like this:
1. Start signal (MCU → DHT)
The MCU initiates communication:
the pin is pulled LOW
duration:
- DHT11: ≥ 18 ms
- DHT22: ≥ 1 ms (in practice, often the same as DHT11)
After that:
the pin is released (HIGH)
the MCU waits for the sensor response
2. ACK signal (DHT → MCU)
The sensor responds with a strictly defined signal:
~80 µs LOW
~80 µs HIGH
If this signal is missing → the sensor did not respond
(wrong pin, wrong sensor type, wiring issue, or reading too fast)
3. Data frame – 40 bits
After the ACK, the sensor sends 40 bits, always in the same order:
Bits Meaning
0–15 Humidity
16–31 Temperature
32–39 Checksum
Each bit is transmitted in two parts:
a) LOW pulse (always the same)
- duration: ~50 µs
- acts as the start of the bit
b) HIGH pulse (value-dependent)
This is the critical part.
Bit HIGH pulse duration
0 ~26–28 µs
1 ~70 µs
The MCU does not “read the pin”. Instead, it:
- measures the HIGH pulse duration
- compares it to a threshold (e.g. ~50 µs)
- decides whether the bit is 0 or 1
If the timing is:
- disturbed
- shortened
- extended
then the bit is wrong.
If one bit is wrong → the checksum fails.
If the checksum fails → the reading is unusable.
This makes DHT extremely sensitive to timing and usage order.
4. Why this is fragile
At this point, it becomes clear why DHT is not a “simple” sensor:
- the difference between 0 and 1 is ~40 microseconds
- the MCU must measure pulses in real time
- any interrupt, delay, or timing mistake can corrupt the read
That’s why:
- reading too fast
- blocking code
- incorrect initialization timing
- wrong sensor type
all lead to the same result: unstable readings.
What a real frame looks like (example)
Example DHT22 reading:
Humidity: 45.6 %
Temperature: 23.8 °C
Raw frame (40 bits):
00000001 11001000 00000000 11101110 10110111
| RH hi | RH lo | T hi | T lo | checksum |
One incorrect bit in this sequence → checksum mismatch → the reading must be discarded.
Why this matters to users
Most users never see this layer.
Libraries hide it — but often fail to protect users from the consequences.
Common mistakes libraries allow (but shouldn’t)
1. Reading the sensor too fast
DHT sensors have an internal refresh interval:
- DHT11: ~1.2 seconds
- DHT22: ~2.0 seconds
If the sensor is read faster than this:
- no new frame is available
- pulse timing becomes unstable
- bits and checksums fail
Many libraries do not enforce this constraint.
The user is expected to “just know” how long to wait.
2. Incorrect error handling (NaN)
When a read fails, most libraries:
- return NaN
- and stop there
Common failure patterns in user code include:
- resetting the MCU on NaN
- using NaN in calculations
- overwriting the last valid value without checking
A more robust behavior looks like this:
- skip the failed cycle
- keep the last valid reading
- continue normal operation
Most libraries do not guide users toward this pattern.
3. First read immediately after begin()
DHT sensors need time to stabilize after initialization.
Some libraries:
- include a delay inside begin()
- others don’t
As a result, the first read often fails,
and users assume the sensor itself is faulty.
4. Blocking code, delay(), and time management
In simple sketches, delay() often works “well enough”.
Once an application:
- does multiple things in loop()
- uses multiple sensors
- uses Serial, I2C, or SPI
blocking code can easily:
- disrupt the effective read interval
- cause sporadic, hard-to-reproduce errors
With timing-sensitive sensors like DHT,
non-blocking time management (millis(), timers)
keeps behavior predictable and stable.
5. Wrong sensor type
A very common beginner mistake:
- a DHT11 is connected
- the code is configured for DHT22 (or vice versa)
The result:
- readings appear to exist
- but are physically nonsensical
Most libraries cannot detect this mismatch.
6. Incorrect initialization location
In some sketches, begin() is:
- called inside loop() instead of setup()
- called multiple times
- called at the wrong point in execution
Re-initializing the sensor can reset pin states and timing assumptions,
leading to unstable or unpredictable communication.
The common denominator
DHT sensors are not inherently unreliable.
The real issue is that many libraries:
- assume users know all protocol constraints
- allow incorrect usage patterns
- do not protect users from common mistakes
In other words, the API is not defensive.
A defensive approach to DHT libraries
After encountering the same problems repeatedly, it became clear to me that DHT sensors usually don’t fail on their own.
The problem is that most libraries don’t protect users from known and frequent mistakes.
A defensive approach can look as simple as this:
#include <myDHT.h>
const int DHT_PIN = 2;
// Auto-detect, no manual configuration
myDHT dht(DHT_PIN);
void setup()
{
Serial.begin(115200);
dht.begin();
}
void loop()
{
// Intentionally reading too fast
Serial.print("Temperature: ");
Serial.print(dht.getTemperature());
Serial.print(" °C, Humidity: ");
Serial.println(dht.getHumidity());
// Too small delay
delay(300);
}
Even in this minimal setup, common issues such as reading too fast,
failed reads, incorrect sensor type, or missing stabilization are handled internally.
(the explicit delay here is optional; safe behavior is enforced internally even without it)
The goal of myDHT is to make this behavior the default —
shifting responsibility for safe sensor usage from application code into the library itself.
When deeper control is needed, the same underlying implementation can be used
without safeguards for analysis, debugging, and advanced use cases.
myDHT does not attempt to “fix” the DHT sensor,
but to prevent a well-known class of usage errors from ever reaching application code.
The point of this post
If you’re using DHT sensors and experiencing problems:
- there’s a good chance you’re not doing anything “wrong”
- your library simply isn’t protecting you
Understanding how DHT really works (bit-banging, timing, frames)
naturally explains why these issues occur.
In the next post, I’ll explain why a simpler (beginner) API can often be safer in practice than an “advanced” one —
and why that’s a conscious design decision, not a compromise.
Reference implementation:
Feedback, experiences, and testing on other boards are more than welcome.
— Toni
