Why DHT11 and DHT22 “lie” to users (and it’s usually not the sensor’s fault)

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

Does real time mean that the MCU must complete reading the 40-bit sensor data within a single uninterrupted time window of approximately 3080–4800 us ((50+27/70)×40), Fig-1? If so, can this operation be implemented on the UNO Q platform as an independent thread when the Zephyr RTOS uses a interrupt driven 1 ms system tick and allocates a 10 ms time slice to each task during round-robin scheduling?


Figure-1:

@tonimatutinovic

Good story,

Think I encountered all the problems you named in the last 15 years when writing DHT libraries (DHTNew is my final one).

You might enjoy (or not) some DHT tools from my hand

Not perse, if your thread switching is fast enough the reading may of course be interrupted, however the DHTxx is much easier to read in an uninterrupted block.

In your math you forget at least the 80 us.

If so, can this operation be implemented on the UNO Q platform as an independent thread when the Zephyr RTOS uses a interrupt driven 1 ms system tick and allocates a 10 ms time slice to each task during round-robin scheduling?

I think not, but feel free to build it and prove me wrong.

@tonimatutinovic

Had a quick look at your repo and you did an impressive job!
I like the setRawBYtes() function, powerful debugging tool.

Thanks, that is a great piece of code!

I'll look at your librsry examples.

Recently for reasons mostly due to an infidelity in the wokwi simulation (I believe) I ended up with this in the loop:

 static unsigned long sensorTmer;

 now = millis();

 if (now - sensorTimer > sensorRate) {
//  if (sensors.isConversionComplete()) {
   sensors.requestTemperatures();

   sensorTimer = now;
 }

 float tempC = sensors.getTempCByIndex(0);

With a greater than it would ever need to be rate constant.

I never got .isConversionComplete() to do what it seems like it should… and gave up running that to ground.

a7

Surely the DHT libraries use blocking timing loops themselves, or timers/interrupts that shouldn't be sensitive to the loop() code blocking while doing other things. If anything, extensive use of non-blocking interrupt-driven code would be WORSE for getting consistent timing of the DHT reads...

(the Arduino DHT11 library is full of blocking using delayMicroseconds(), but it doesn't seem to disable other interrupts. So a badly time interrupt exceeding some 10s of us execution time can potentially be a bad thing. (AVR millisecond interrupt is pretty minimal, but runs about 6us...))

Good question.

I don’t yet have enough hands-on experience with RTOS-based scheduling on small MCUs
to give a fully confident, practical answer here.

So far, my experience matches what Rob pointed out:
for DHT-style timing-based protocols, uninterrupted reads are by far the most reliable option.
Once frequent interrupts and task switching are involved, timing margins become very tight.

Since this post focuses mainly on bare-metal and simple scheduling use cases,
I’ve mostly approached DHT reading in that context so far.

Thanks, Rob.

I’ve looked through DHTNew and the simulator. Both are very clean and practical.
I like how directly they expose the timing behaviour.

I’m glad you found the setRawBytes() feature useful — it helped a lot during debugging.

Thanks for taking the time to look at my repo.

Thanks! Glad you found it useful.

Thanks for sharing that.

Using a time guard with millis() is often the most predictable approach,
especially when timing-dependent behavior isn’t fully deterministic.

Falling back to explicit timing control usually makes things more robust.

Curious to hear your thoughts once you’ve had a look at the examples.

Good point, you’re absolutely right that the low-level DHT read itself is inherently blocking and timing-critical.

What I mainly meant in that section was the application-level timing around the reads: when and how often the sensor is polled, and how other blocking code can indirectly break those assumptions as the sketch grows. Many libraries rely on the user to manage that correctly, and once timing slips, reads can start happening too early, too late, or in bursts.

And yes, excessive or poorly-timed interrupts can definitely make things worse without explicit masking during the critical section.

Thanks for adding that nuance, it’s an important clarification.

Quick update:

I’ve just released v2.0.4 of myDHT, which includes a fix for a compilation issue on ESP32 and improves compatibility with stricter toolchains.

The ESP32 issue was reported by a user while working with a multi-sensor setup, which highlighted a compiler-specific problem — and also reinforced the need for broader real-world testing.

All examples are now continuously built and verified via CI (GitHub Actions) across AVR, ESP32, ESP8266 and SAMD platforms.

So far, the library has been tested on:

  • Arduino Uno / Nano (ATmega328P)
  • DHT11 and DHT22

If anyone is running this on other boards or setups, especially:

  • ESP32 / ESP8266
  • SAMD-based boards
  • multiple sensors (e.g. 2× DHT22)
  • longer wires or noisier environments

it would be useful to hear how it behaves in those conditions.

GitHub: