Components · 14 min read

Water Level Sensor Arduino: Calibration, Overflow Alarm and Pump

How to use a water level sensor with Arduino: empty/half/full calibration, pin powering against corrosion, buzzer overflow alarm, pump control, ESP32/Pico code.

A water level sensor is the cheapest and easiest way to detect how high the water is in a glass, an aquarium, a plant saucer or a small tank with an Arduino. As the exposed conductive traces on the board go under water, the voltage on its analog output rises; the Arduino reads that voltage with analogRead() and decides whether the water is “empty”, “half” or “full”. It connects with three wires, works with a few lines of code and is the starting point for many projects, from overflow alarms to automatic pump control.

In this guide you'll learn what's inside the module, why the reading never reaches 1023, how to do an empty/half/full calibration and how to prevent the corrosion that eats the sensor within a few weeks. Then you'll build a buzzer overflow alarm and a pump control that refills a tank on its own, compare the sensor briefly with a float switch and the contactless XKC-Y25, and finally see how to do the same job with the ESP32 and Raspberry Pi Pico.

Arduino water level sensor module
Water level sensor module: the interleaved conductive traces at the bottom go into the water, while the S, + and − pins at the top connect to the Arduino; the electronics must always stay dry.

What is a water level sensor and how does it work?

The lower part of the module carries ten conductive traces that don't touch each other and interlock like the teeth of two combs. Five of them are power traces tied to the + supply; the other five are sense traces that lead to the base of the S8050 NPN transistor on the top of the board. When the board is dry there's only air between the traces, the transistor is off and the S pin sits close to 0 V. Once the board is in water, the water bridges the power and sense traces; a small current flows into the transistor's base, the transistor starts to conduct and the voltage on the S output, taken from the emitter, rises. The higher the water, the longer the wetted length of trace, the lower the resistance between the traces and the higher the output voltage.

This way of working has three important consequences. First, the sensor doesn't really measure water but the water's conductivity: the dissolved minerals in tap water push the reading up, while pure (distilled) water gives almost nothing. Second, because the output comes from a transistor working as an emitter follower, the voltage never reaches the supply; a fully wet sensor powered at 5 V usually reads around 500–650 on an Arduino Uno and never gets anywhere near 1023. Third, the relationship isn't linear: the value climbs quickly over the first few millimetres and then slows down more and more. So this sensor isn't a ruler with millimetre accuracy but a practical indicator that reliably tells a few levels apart.

FeatureValue
Operating voltage3.3–5 V DC
Operating currentLess than 20 mA (highest when fully wet)
OutputAnalog voltage (S pin), rises as the water rises
Sensing areaAbout 40 × 16 mm, so at most about 4 cm of water height
Board sizeAbout 62 × 20 mm
Operating temperature10–30 °C (manufacturer's figure)
PinsS (signal), + (VCC), − (GND)

Water level sensor wiring: Arduino Uno, ESP32 and Pico

Sensor pinArduino UnoESP32Raspberry Pi Pico
S (signal)A0GPIO 34 (ADC1)GP26 (ADC0)
+ (VCC)D7 (powers it only while measuring)GPIO 25GP15
− (GND)GNDGNDGND
Don't connect the sensor's + pin permanently to 5V. When direct current keeps flowing through the wet traces, electrolysis starts: the plating on the traces tied to + dissolves, the traces darken and within a few weeks the readings start to drift. Also, only the lower part of the board with the conductive traces may go into the water; if the upper part with the transistor, the LED and the pins gets wet, you get a short circuit.

The easiest way to prevent corrosion is to power the sensor from a digital pin, as shown in the table: the code sets the pin HIGH just before a measurement, waits 10 ms, reads the value and sets the pin LOW again. In a system that measures once a second, the traces are energised only 1 percent of the time, which slows the wear by roughly a hundred times. When fully wet the module draws at most around 20 mA. The recommended limit for an Arduino Uno pin is 20 mA and the absolute maximum is 40 mA, and the pin is on for only a few milliseconds, so this connection is safe; ESP32 pins also supply this current easily. The Pico's pins, however, run with a lower default drive-strength setting, so when the sensor is fully wet the pin voltage sags a little and the readings get smaller. As long as you calibrate with the same wiring that's not a problem; if you want steadier values, you can switch the sensor's + with a small PNP transistor such as the BC557: the emitter goes to the board's 3V3 pin, the collector to the sensor's +, and the base to the GPIO pin through a 1 kΩ resistor, and the sensor then turns on when the pin is LOW. To pick a suitable transistor, have a look at the transistor tool.

Step by step: your first reading with Arduino

  1. Connect the sensor's S pin to A0, the + pin to D7 and the − pin to GND. Hold the board upright with the traces pointing down.
  2. Open the code below in the Arduino IDE, select Arduino Uno as the board, pick the right port and upload.
  3. Open the Serial Monitor and set it to 9600 baud. Check that the value is close to 0 while the sensor is dry.
  4. Lower the sensor slowly into a glass of water: first just the tip, then halfway, then up to the line where the traces end. At each step wait a few seconds and write down the value.
C++ (Arduino IDE)
1const int SENSOR_PIN = A0;   // S → A0
2const int POWER_PIN = 7;     // + → D7 (yalnızca ölçerken besle / power only while measuring)
3
4int readSensor() {
5  digitalWrite(POWER_PIN, HIGH);        // sensörü aç / power the sensor
6  delay(10);                            // değerin oturmasını bekle / let the reading settle
7  long sum = 0;
8  for (int i = 0; i < 8; i++) {         // 8 ölçümün ortalaması / average of 8 readings
9    sum += analogRead(SENSOR_PIN);
10  }
11  digitalWrite(POWER_PIN, LOW);         // sensörü kapat / power off
12  return sum / 8;
13}
14
15void setup() {
16  pinMode(POWER_PIN, OUTPUT);
17  digitalWrite(POWER_PIN, LOW);         // kapalı başla / start powered off
18  Serial.begin(9600);
19}
20
21void loop() {
22  int raw = readSensor();
23  Serial.print("Ham deger / Raw value: ");
24  Serial.println(raw);
25  delay(1000);                          // saniyede bir ölç / measure once a second
26}

The readSensor() function keeps the sensor on for only about 11 ms per measurement. The 10 ms wait lets the transistor and the water between the traces settle to a stable value; then the average of 8 readings smooths out the small fluctuations of individual samples. On the Uno each analogRead() takes about 0.1 ms, so 8 readings take less than 1 ms. When you stir the water or shake the glass you'll see the values jump around in the Serial Monitor; that's normal, because the moving surface keeps changing the wetted length of the traces.

Empty, half, full: how to calibrate

Don't use the thresholds from code you find online as they are; every sensor, every kind of water and every supply voltage gives different values. Calibrate in the container where the sensor will really be used and with the same water: aquarium water for an aquarium, fertilised water for a fertilised irrigation tank. Draw lines at 0, 10, 20, 30 and 40 mm on the sensor with a marker, dip the board to each line, wait 3–4 seconds for the value to settle and write down the number in the Serial Monitor. The table below shows typical ranges for tap water and a 5 V supply; note your own values next to them.

LevelImmersion depthTypical Uno value (5 V, tap water)
Empty0 mm (dry)0–20
LowAbout 5–10 mm250–400
HalfAbout 20 mm400–480
FullAbout 40 mm (line where the traces end)480–650

What stands out in the table is how fast the values rise in the first centimetre and then bunch up: there can be a difference of several hundred units between 0 and 10 mm but only a few dozen between 20 and 40 mm. That's why, instead of drawing a single straight line with map(), piecewise linear interpolation, which uses a separate ratio between each pair of measuring points, gives a much more accurate result. The code below keeps the five points you measured in two arrays, converts the raw value to millimetres and prints the level by name. All you need to do is write your own readings into the RAW array in ascending order.

C++ (Arduino IDE)
1const int SENSOR_PIN = A0;
2const int POWER_PIN = 7;
3
4// Kendi kalibrasyon ölçümlerin / your own calibration readings
5const int N = 5;
6const int RAW[N] = {0, 330, 440, 490, 520};   // ham değer, küçükten büyüğe / raw value, ascending
7const int MM[N]  = {0, 10, 20, 30, 40};       // batma derinliği (mm) / immersion depth (mm)
8
9int readSensor() {
10  digitalWrite(POWER_PIN, HIGH);
11  delay(10);
12  long sum = 0;
13  for (int i = 0; i < 8; i++) sum += analogRead(SENSOR_PIN);
14  digitalWrite(POWER_PIN, LOW);
15  return sum / 8;
16}
17
18int rawToMm(int raw) {
19  if (raw <= RAW[0]) return MM[0];
20  for (int i = 1; i < N; i++) {
21    if (raw <= RAW[i]) {                      // iki nokta arasında oran kur / interpolate between two points
22      return map(raw, RAW[i - 1], RAW[i], MM[i - 1], MM[i]);
23    }
24  }
25  return MM[N - 1];                           // en üst noktanın üstü / above the top point
26}
27
28const char* levelName(int mm) {
29  if (mm < 3)  return "Bos / Empty";
30  if (mm < 15) return "Az / Low";
31  if (mm < 30) return "Yarim / Half";
32  return "Dolu / Full";
33}
34
35void setup() {
36  pinMode(POWER_PIN, OUTPUT);
37  digitalWrite(POWER_PIN, LOW);
38  Serial.begin(9600);
39}
40
41void loop() {
42  int raw = readSensor();
43  int mm = rawToMm(raw);
44  Serial.print(raw);
45  Serial.print(" -> ");
46  Serial.print(mm);
47  Serial.print(" mm, ");
48  Serial.println(levelName(mm));
49  delay(1000);
50}
When you take the sensor out of the water, the value doesn't drop to 0 straight away; the thin film of water and the droplets left between the traces keep the reading noticeably above zero for a while. So keep the “empty” threshold a little above 0, and for on/off decisions such as an alarm use two separate thresholds: one to switch on and one to switch off. This method is called hysteresis, and it stops a relay or buzzer from clicking on and off every second while the level wobbles right around the threshold.

Overflow alarm with a buzzer

One of the most useful jobs for a water level sensor is warning you about an overflow: placed under the washing machine or the boiler, in a plant saucer, in an air conditioner's drain tray or on the rim of a bucket that's filling up, the sensor starts the alarm as soon as the water touches its tip. Even the “low” level from the calibration table is enough for this. The code below uses an active buzzer and the Uno's built-in LED; when the value passes ALARM_ON the buzzer beeps every quarter of a second, and when the value drops below ALARM_OFF it goes quiet. Because it uses millis() instead of delay() for the timing, the measurement and the buzzer don't block each other. For the types of buzzer see What Is a Buzzer?, and for the millis() idea see millis() and delay().

C++ (Arduino IDE)
1const int SENSOR_PIN = A0;   // S → A0
2const int POWER_PIN = 7;     // + → D7
3const int BUZZER_PIN = 8;    // aktif buzzer + ucu / active buzzer +
4const int LED_PIN = 13;      // Uno'nun dahili LED'i / Uno's built-in LED
5
6const int ALARM_ON = 200;    // bu değer ve üstü taşma / at or above: overflow (kendin ölç / measure yours)
7const int ALARM_OFF = 100;   // bunun altına inince alarm biter / alarm clears below this
8
9bool alarmActive = false;
10bool buzzerOn = false;
11unsigned long lastRead = 0;
12unsigned long lastBeep = 0;
13
14int readSensor() {
15  digitalWrite(POWER_PIN, HIGH);
16  delay(10);
17  long sum = 0;
18  for (int i = 0; i < 8; i++) sum += analogRead(SENSOR_PIN);
19  digitalWrite(POWER_PIN, LOW);
20  return sum / 8;
21}
22
23void setup() {
24  pinMode(POWER_PIN, OUTPUT);
25  pinMode(BUZZER_PIN, OUTPUT);
26  pinMode(LED_PIN, OUTPUT);
27  digitalWrite(POWER_PIN, LOW);
28  Serial.begin(9600);
29}
30
31void loop() {
32  unsigned long now = millis();
33
34  if (now - lastRead >= 1000) {                       // saniyede bir ölç / measure once a second
35    lastRead = now;
36    int level = readSensor();
37    if (!alarmActive && level >= ALARM_ON) alarmActive = true;    // taşma başladı / overflow started
38    if (alarmActive && level < ALARM_OFF) alarmActive = false;    // su çekildi / water receded
39    Serial.print("Seviye / Level: ");
40    Serial.println(level);
41  }
42
43  if (alarmActive) {
44    if (now - lastBeep >= 250) {                      // 250 ms öt, 250 ms sus / 250 ms on, 250 ms off
45      lastBeep = now;
46      buzzerOn = !buzzerOn;
47      digitalWrite(BUZZER_PIN, buzzerOn ? HIGH : LOW);
48    }
49  } else if (buzzerOn) {
50    buzzerOn = false;
51    digitalWrite(BUZZER_PIN, LOW);
52  }
53  digitalWrite(LED_PIN, alarmActive ? HIGH : LOW);
54}

Pump control: refilling a tank on its own

The second classic application is automatic refilling that keeps the level within a band. You might want to top up the water that evaporates from an aquarium from a spare bucket, keep your pet's water bowl full or feed a greenhouse tank. The logic is simple: when the level drops below START_BELOW the pump runs, and when it reaches STOP_ABOVE it stops. The gap between these two thresholds is the hysteresis that keeps the pump from switching on and off constantly. Remember that the sensor only has a sensing area of about 4 cm; mount the board at a height where the band of water you want to hold falls inside those 4 cm.

6 V mini submersible water pump
3–6 V mini submersible pump: it works under water and draws about 100–250 mA, so it's driven through a relay or a MOSFET, not straight from an Arduino pin.

The pump draws far more current than an Arduino pin can supply, so drive it with a relay module or with a logic-level MOSFET and a flyback diode, power the pump from a separate supply and connect the GNDs of both supplies together. For details see What Is a Relay Module?, using a MOSFET and the mini water pump. The most important part of the code is a safety measure: if the pump runs for the whole MAX_RUN_MS period and the level still hasn't reached the target, the system locks the pump out and warns you with short beeps. The hose may have slipped out, the spare bucket may be empty or the sensor may have moved; without this lockout the pump could run dry for hours or flood the whole place.

C++ (Arduino IDE)
1const int SENSOR_PIN = A0;
2const int POWER_PIN = 7;
3const int PUMP_PIN = 8;                 // röle IN / relay IN
4const int BUZZER_PIN = 9;               // aktif buzzer / active buzzer
5const bool RELAY_ACTIVE_LOW = true;     // çoğu röle modülü LOW ile çeker / most relay modules switch on LOW
6
7const int START_BELOW = 300;            // bunun altında doldur / start filling below this (kendin ölç / measure yours)
8const int STOP_ABOVE = 480;             // bu değerde dur / stop at this value
9const unsigned long MAX_RUN_MS = 30000; // en uzun çalışma 30 sn / max run time 30 s
10
11bool pumpOn = false;
12bool fault = false;
13unsigned long pumpStart = 0;
14
15int readSensor() {
16  digitalWrite(POWER_PIN, HIGH);
17  delay(10);
18  long sum = 0;
19  for (int i = 0; i < 8; i++) sum += analogRead(SENSOR_PIN);
20  digitalWrite(POWER_PIN, LOW);
21  return sum / 8;
22}
23
24void setPump(bool on) {
25  pumpOn = on;
26  bool pinHigh = RELAY_ACTIVE_LOW ? !on : on;
27  digitalWrite(PUMP_PIN, pinHigh ? HIGH : LOW);
28}
29
30void setup() {
31  setPump(false);              // pin çıkış olmadan önce kapalı konuma al / set off before the pin becomes an output
32  pinMode(PUMP_PIN, OUTPUT);
33  pinMode(POWER_PIN, OUTPUT);
34  pinMode(BUZZER_PIN, OUTPUT);
35  digitalWrite(POWER_PIN, LOW);
36  Serial.begin(9600);
37}
38
39void loop() {
40  if (fault) {                 // arıza: pompa kilitli, kesik kesik öt / fault: pump locked out, beep
41    digitalWrite(BUZZER_PIN, HIGH);
42    delay(200);
43    digitalWrite(BUZZER_PIN, LOW);
44    delay(1800);
45    return;                    // yeniden başlatmak için reset / press reset to restart
46  }
47
48  int level = readSensor();
49
50  if (!pumpOn && level < START_BELOW) {
51    setPump(true);             // seviye düştü, doldur / level low, start filling
52    pumpStart = millis();
53  } else if (pumpOn && level >= STOP_ABOVE) {
54    setPump(false);            // istenen seviyeye ulaşıldı / target level reached
55  } else if (pumpOn && millis() - pumpStart > MAX_RUN_MS) {
56    setPump(false);            // süre doldu, seviye yükselmedi / timed out, level did not rise
57    fault = true;
58  }
59
60  Serial.print("Seviye / Level: ");
61  Serial.print(level);
62  Serial.println(pumpOn ? "  pompa / pump ON" : "  pompa / pump OFF");
63  delay(500);
64}
Water and electricity in the same project: place the Arduino, the relay and the power supply above and away from the water containers, and insulate wire joints with heat-shrink tubing or hot glue. If you're going to use a pump or solenoid valve that runs on mains voltage, have a qualified person make the mains connection and work on a circuit protected by a residual-current device (RCD). Small submersible pumps overheat and fail quickly if they run dry; putting a float switch in the spare bucket as well is a good second layer of protection.

Water level sensor vs float switch vs XKC-Y25

FeatureWater level sensor (conductive traces)Float switchXKC-Y25 (contactless)
Sensing methodConductivity of the waterFloating magnet and reed contactChange in capacitance behind the container wall
OutputAnalog (a few levels)Open or closed contactDigital (liquid present or not)
Measuring rangeAbout 0–4 cmOne point (mounting height)One point (mounting height)
Contact with waterYes, traces in the waterYes, body in the waterNo, sticks to the outside of the container
CorrosionYes; powering from a pin is a mustPractically noneNone
Supply3.3–5 VNot needed (passive contact)5–24 V, about 5 mA
Best forExperiments, overflow alarms, small containersPermanent tanks, pump protectionClosed plastic or glass tanks, corrosive liquids
PriceCheapestCheapMore expensive than the others

A float switch consists of a stem with a reed contact inside and a float with a magnet that slides along it. As the water rises the float lifts, the magnet lines up with the reed contact and the contact closes or opens. By fitting the float upside down on the stem you can swap between normally open (NO) and normally closed (NC) behaviour. To connect it to an Arduino, wire one lead to a digital pin and the other to GND and set the pin to INPUT_PULLUP; since there's no electrolysis wear, it lasts much longer in permanent installations. The contacts of small models are usually limited to around 0.5 A and 10 W, so drive the pump through a relay, not directly. For how a reed contact works, see the Reed Switch article.

Float switch water level sensor
Float switch: the magnetic float rises with the water and opens or closes the reed contact in the stem.
C++ (Arduino IDE)
1const int FLOAT_PIN = 2;    // bir kablo D2'ye, diğeri GND'ye / one wire to D2, the other to GND
2
3void setup() {
4  pinMode(FLOAT_PIN, INPUT_PULLUP);   // dahili pull-up direnci / internal pull-up resistor
5  Serial.begin(9600);
6}
7
8void loop() {
9  // Kontak kapalıyken pin LOW okunur / the pin reads LOW when the contact is closed
10  if (digitalRead(FLOAT_PIN) == LOW) {
11    Serial.println("Kontak kapali / Contact closed");
12  } else {
13    Serial.println("Kontak acik / Contact open");
14  }
15  delay(500);
16}

The XKC-Y25, on the other hand, is a capacitive sensor that never touches the water: you stick it to the outside of a plastic or glass tank and it detects whether there's liquid behind the wall. According to the manufacturer it runs on 5–24 V, draws about 5 mA, can sense through non-metallic walls up to 13 mm thick and responds in about 500 ms. The high level of its output equals the supply voltage, so when you connect an XKC-Y25 powered at 5 V to an ESP32 or Pico you should use a voltage divider (the voltage divider tool works out the right resistors). We cover the sensitivity adjustment and the models with different outputs in detail in the XKC-Y25 contactless level sensor article.

XKC-Y25 contactless liquid level sensor
XKC-Y25: a capacitive sensor stuck to the outside of the tank; it never touches the water, so it never corrodes.

Monitoring the water level from your phone with the ESP32

With the ESP32 you can bring the measurement to your phone over Wi-Fi. Always connect the sensor's S pin to an ADC1 pin (GPIO 32–39); ADC2 pins can't be read while Wi-Fi is on. The ESP32's ADC is 12-bit, so values range from 0 to 4095, and because the sensor is powered at 3.3 V the numbers can't be compared with the Uno's; you'll need to calibrate again. The ESP32's ADC isn't fully linear near 0 V and near the top of its range, but since what matters here is the difference between levels rather than the absolute voltage, that's not a problem. The code below takes a reading every two seconds and serves a small web page that refreshes itself every five seconds, which you can open from your phone on the same network.

C++ (Arduino IDE)
1#include <WiFi.h>
2#include <WebServer.h>
3
4const char* WIFI_SSID = "WIFI_ADIN";      // ağ adın / your network name
5const char* WIFI_PASS = "WIFI_SIFREN";    // ağ şifren / your network password
6
7const int SENSOR_PIN = 34;   // S → GPIO 34 (ADC1, Wi-Fi ile çalışır / works with Wi-Fi)
8const int POWER_PIN = 25;    // + → GPIO 25
9const int HALF = 1500;       // yarım eşiği / half threshold (kendin ölç / measure yours)
10const int FULL = 2000;       // dolu eşiği / full threshold (kendin ölç / measure yours)
11
12WebServer server(80);
13int lastRaw = 0;
14unsigned long lastRead = 0;
15
16int readSensor() {
17  digitalWrite(POWER_PIN, HIGH);
18  delay(10);
19  long sum = 0;
20  for (int i = 0; i < 16; i++) sum += analogRead(SENSOR_PIN);   // 0–4095
21  digitalWrite(POWER_PIN, LOW);
22  return sum / 16;
23}
24
25const char* levelName(int raw) {
26  if (raw < 100) return "Bos / Empty";
27  if (raw < HALF) return "Az / Low";
28  if (raw < FULL) return "Yarim / Half";
29  return "Dolu / Full";
30}
31
32void handleRoot() {
33  String html = "<!DOCTYPE html><html><head><meta charset='utf-8'>"
34                "<meta name='viewport' content='width=device-width'>"
35                "<meta http-equiv='refresh' content='5'></head><body>"
36                "<h1>Su seviyesi / Water level</h1><h2>";
37  html += levelName(lastRaw);
38  html += "</h2><p>Ham deger / Raw: ";
39  html += lastRaw;
40  html += "</p></body></html>";
41  server.send(200, "text/html", html);
42}
43
44void setup() {
45  Serial.begin(115200);
46  pinMode(POWER_PIN, OUTPUT);
47  digitalWrite(POWER_PIN, LOW);
48  WiFi.begin(WIFI_SSID, WIFI_PASS);
49  while (WiFi.status() != WL_CONNECTED) {   // bağlanana kadar bekle / wait until connected
50    delay(500);
51    Serial.print(".");
52  }
53  Serial.println();
54  Serial.print("Adres / Address: http://");
55  Serial.println(WiFi.localIP());
56  server.on("/", handleRoot);
57  server.begin();
58}
59
60void loop() {
61  server.handleClient();
62  if (millis() - lastRead >= 2000) {        // 2 sn'de bir ölç / measure every 2 s
63    lastRead = millis();
64    lastRaw = readSensor();
65  }
66}

Before uploading, replace WIFI_SSID and WIFI_PASS with your own network details; the ESP32 only connects to 2.4 GHz networks. When you open the Serial Monitor at 115200 baud, just type the address it shows into your phone's browser. If you'd like a notification on your phone when the level is “full”, you can turn the same reading into a message with the method from the ESP32 Telegram bot article.

Overflow alarm with Raspberry Pi Pico and MicroPython

On the Raspberry Pi Pico the GP26, GP27 and GP28 pins are used for analog readings. MicroPython's read_u16() function scales the Pico's 12-bit measurement to the 0–65535 range, so your thresholds will be much bigger numbers than on the Uno. The code below powers the sensor from GP15, reads the S output on GP26 and gives an overflow warning with three short beeps from an active buzzer on GP14. As with the first sketch, set the thresholds from your own measurements.

MicroPython
1from machine import ADC, Pin
2import time
3
4sensor = ADC(26)                    # GP26 (ADC0) → S
5power = Pin(15, Pin.OUT, value=0)   # GP15 → sensör + / sensor +
6buzzer = Pin(14, Pin.OUT, value=0)  # GP14 → aktif buzzer / active buzzer
7
8ALARM_ON = 20000    # bu değer ve üstü taşma / at or above: overflow (kendin ölç / measure yours)
9ALARM_OFF = 10000   # bunun altında alarm biter / alarm clears below this
10
11def read_level(samples=8):
12    power.value(1)
13    time.sleep_ms(10)               # değerin oturmasını bekle / let the reading settle
14    total = 0
15    for _ in range(samples):
16        total += sensor.read_u16()  # 0–65535
17    power.value(0)
18    return total // samples
19
20alarm = False
21while True:
22    level = read_level()
23    if not alarm and level >= ALARM_ON:
24        alarm = True
25    elif alarm and level < ALARM_OFF:
26        alarm = False
27    print("Seviye / Level:", level, "ALARM" if alarm else "")
28    if alarm:
29        for _ in range(3):          # üç kısa bip / three short beeps
30            buzzer.value(1)
31            time.sleep_ms(150)
32            buzzer.value(0)
33            time.sleep_ms(150)
34    else:
35        time.sleep(1)

Project ideas with a water level sensor

  • Plant saucer overflow warning: A buzzer that sounds when water collects in the saucer after watering; combine it with a soil moisture sensor and you have a complete watering system.
  • Automatic aquarium top-off: A time-limited refill system that replaces evaporated water from a spare bucket with a mini pump.
  • Pet water bowl: A reminder that lights an LED or sends a notification to your phone via the ESP32 when the bowl runs low.
  • Water leak detector: A leak alarm with the sensor laid flat on the floor under a washing machine or boiler, or in a basement.
  • Smart greenhouse tank: Protection that watches the level of the irrigation tank and locks the pump out when it runs dry; it slots easily into the smart greenhouse project.
  • Rain barrel gauge: A panel that shows the level as empty, half or full on an I2C LCD display; you can also add a rain sensor to catch the moment the rain starts.

Common mistakes and how to fix them

SymptomLikely causeFix
The value is always 0, even in waterThe + pin isn't powered (POWER_PIN never goes HIGH) or S is on the wrong pinCheck the pinMode and digitalWrite lines and that S is wired to an analog pin
The value jumps around randomly even when dryThe S pin is floating or GND isn't connectedCheck all three wires; the GND must be common
It never reaches 1023, even fully submergedThat's normal; the output comes from an emitter followerSet your thresholds from the highest value you measured yourself
No reading at all in pure waterDistilled or purified water has very low conductivityUse an XKC-Y25 or a float switch with this kind of water
The values drop or drift over a few weeksElectrolysis and deposits on the tracesPower the sensor from a pin, clean the traces with a soft brush and replace the sensor if needed
The pump or buzzer keeps toggling at the thresholdA single threshold is used; the moving surface makes the value wobbleUse two separate thresholds for on and off (hysteresis) and average the readings
On the ESP32 the value can't be read once Wi-Fi connectsThe sensor is on an ADC2 pinMove S to an ADC1 pin between GPIO 32 and 39
To make the sensor last longer, place it upright in the water, submerge only the part with the traces and coat the electronics at the top with hot glue or clear lacquer; don't coat the traces themselves, though, or they can't measure conductivity. For a permanent installation that will sit in water for months, the most robust solution is to use this sensor for the prototype and switch to a float switch or a contactless sensor in the final system.

The water level sensor teaches analog readings, calibration, hysteresis and real-world problems such as corrosion with a single cheap module. If you need to measure the level of a large tank centimetre by centimetre, you can move to an ultrasonic solution such as the HC-SR04 distance sensor. In the BlueGrays app you'll find the smart irrigation station and buzzer alarm projects with step-by-step circuit diagrams and code, and you can easily add the water level sensor to them.

Frequently asked questions

How does a water level sensor work?

As the interleaved conductive traces on the board go into the water, the water bridges them and current flows into the base of the S8050 transistor on the board. The more the transistor conducts, the higher the voltage on the S pin; the Arduino reads that voltage with analogRead() to tell how high the water is.

Why does my water level sensor corrode?

When direct current keeps flowing through the wet traces, electrolysis starts and the plating on the traces tied to + dissolves. If you power the sensor from a digital pin instead of 5V and switch it on for only a few milliseconds per reading, the wear slows down dramatically and the sensor lasts for months.

Does a water level sensor work in distilled water?

Not really. Because the sensor measures the water's conductivity, distilled or purified water without dissolved minerals gives very low readings. For this kind of water, sensors that don't depend on conductivity, such as the contactless XKC-Y25 or a float switch, are the better choice.

Why doesn't the water level sensor reading reach 1023?

The S output is taken from the transistor's emitter, so the voltage stays below the supply, and the base current is limited by the resistance of the water. On an Arduino Uno at 5 V a fully wet sensor usually reads around 500–650, so set your thresholds based on that maximum.

Can a water level sensor be fully submerged?

No. Only the lower part with the conductive traces should go into the water; if the upper part with the transistor, LED and pins gets wet, it shorts out and the sensor reads wrongly or fails. Protecting the electronics with hot glue or lacquer makes it last longer.

How do you measure the level of a large water tank?

This sensor only covers a range of about 4 cm, so it isn't suitable for large tanks. To measure the level continuously, waterproof ultrasonic sensors such as the JSN-SR04T mounted in the tank lid are used; to watch just a few critical points, float switches are placed at different heights.

See all projects
Using a Buzzer
#13Beginner

Using a Buzzer

Ever wondered how a doorbell or an alarm makes its sound? The Arduino makes a small buzzer beep at short intervals: beep, pause, beep, pause. You see how sound is made with electricity and take the fi

4.7(89)
161
Measuring Light Intensity with an LDR
#11Beginner

Measuring Light Intensity with an LDR

Want to see how bright your room is as a number? The LDR, a small part that reacts to light, measures the light around it, and the Arduino writes the value to your computer screen every half second. C

4.6(81)
139
Reading Light Intensity with the ESP32 LDR Module
#113Beginner

Reading Light Intensity with the ESP32 LDR Module

Real-time monitoring of the LDR module's raw ADC value and relative light percentage calculated from dark and light reference values in the Serial Monitor. This percentage is not a lux measurement. Teaches the module’s calibration logic. It is ideal to complete this before Project 10, as it is used to determine the threshold value.

4.6(36)
55
Smart Irrigation Station
#83Advanced
Premium

Smart Irrigation Station

A gardener that waters your plants on its own, so they don't dry out while you're on vacation. A soil moisture sensor measures how wet the soil is; when it gets dry, the Arduino runs the water pump fo

5.0(42)
57
Reading a Reed Switch
#37Beginner

Reading a Reed Switch

Ever wondered about the magnetic switch inside door and window alarms? When a magnet comes near the reed relay module, the tiny switch inside it closes; the Arduino reads it once a second and prints 0

4.4(21)
27
ESP32 Asynchronous Web Server (with IP Display on LCD)
#129Advanced

ESP32 Asynchronous Web Server (with IP Display on LCD)

In this project, we’re turning the ESP32 into an asynchronous web server that you can control from your phone or computer on the same Wi-Fi network. Thanks to the ESPAsyncWebServer library, the server can process multiple requests simultaneously (unlike the classic WebServer.h, which blocks on a single request). You’ll be able to open the ESP32’s IP address in a browser and click the ON/OFF buttons (the /on and /off routes) to turn the LED connected to GPIO 5 on and off.

4.8(16)
26
ESP32 Motion Alarm System (PIR + Buzzer + LED + Telegram)
#145Advanced
Premium

ESP32 Motion Alarm System (PIR + Buzzer + LED + Telegram)

A smart security alarm system that activates the buzzer and LED when the PIR sensor detects motion, while simultaneously sending an instant notification to the owner via Telegram. It includes a 30-second cooldown to limit the frequency of repeated alarms; this mechanism does not validate PIR detections or eliminate false triggers. Suitable for home security, garage monitoring, warehouses, or workrooms. With ESP32-CAM, photo notifications can be added; with the classic ESP32 DevKit V1, text notif

0.0(0)
9