Skip to content
Tutorial
13 min read
October 17, 2026

ESP32 GPIO Input vs Output: Beginner Guide to LEDs, Buttons & Pull-Ups

Learn ESP32 digital input and output from scratch: blink an LED, read a button with INPUT_PULLUP, understand pull-up and pull-down resistors, debounce properly, calculate LED resistors and avoid the pin mistakes that break beginner projects.

Every ESP32 project, from a blinking LED to a full smart home node, comes down to two simple actions: driving a pin high or low (output) and reading whether a pin is high or low (input). Get these two right, including pull-up resistors, debouncing and the 3.3V limits, and a huge share of beginner problems disappear before they start.

This guide teaches digital GPIO from the ground up with working examples: blinking an LED, reading a button the correct way, toggling an LED with a button press, debouncing without blocking your code, picking the right resistor and keeping your pins safe. It assumes nothing beyond an ESP32 board and the Arduino IDE. Pin numbers refer to a standard DevKit; the ESP32 pinout guide shows where each one is on your board.

Image pending: ESP32 on a breadboard with an LED on GPIO 23 and a push button on GPIO 27, wired with color-coded jumper wires

Input and Output in One Table

OutputInput
What the ESP32 doesDrives the pin to 3.3V (HIGH) or 0V (LOW)Measures whether the pin is near 3.3V or near 0V
Arduino calldigitalWrite(pin, HIGH or LOW)digitalRead(pin) returns HIGH or LOW
Set up withpinMode(pin, OUTPUT)pinMode(pin, INPUT), INPUT_PULLUP or INPUT_PULLDOWN
Typical useLEDs, transistors, relay modules, buzzersButtons, switches, digital sensors
Voltage3.3V, never 5VMust stay between 0V and 3.3V

Two rules to remember forever:

  1. ESP32 pins are 3.3V. A 5V signal on an input can damage the chip over time or at once. Use a voltage divider or a level shifter for any 5V output.
  2. A pin can only supply a small current, roughly 20 mA is a sensible limit. That runs an LED. It does not run a motor, a relay coil or a buzzer. Those need a transistor or MOSFET.

Not every pin is equally friendly. GPIO 34, 35, 36 and 39 are input only, GPIO 6 to 11 are connected to the flash chip and must not be used, and a few pins affect booting. The full picture is in the ESP32 pinout guide. For everything below, GPIO 23 (LED) and GPIO 27 (button) are safe choices.

Output: Blink an LED

What the resistor does

An LED has almost no resistance of its own, so without a resistor it draws as much current as the pin can give and will burn out the LED or the pin. Always put a resistor in series.

The formula is R = (Vsupply − Vled) / I. With a 3.3V supply, a red LED that drops about 2.0V and a comfortable 8 mA:

R = (3.3 − 2.0) / 0.008 = about 160 Ω

So 220 Ω is a safe, common choice (a little dimmer than the maximum, but plenty bright). Blue and white LEDs drop around 3.0V, leaving very little voltage for the resistor, so use a smaller value such as 100 Ω and expect a dimmer glow at 3.3V.

Wiring

GPIO 23 to the 220 Ω resistor, the resistor to the LED's long leg (anode), and the LED's short leg (cathode) to GND.

Code

const int LED_PIN = 23;

void setup() {
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_PIN, HIGH);   // LED on
  delay(500);
  digitalWrite(LED_PIN, LOW);    // LED off
  delay(500);
}

If the LED never lights, flip it around (LEDs only work one way), check the resistor is in the circuit and confirm the pin number matches the physical pin you used.

The onboard LED on most DevKit boards is on GPIO 2, so const int LED_PIN = 2; blinks it with no wiring at all, a good first test that your upload works.

Image pending: Wiring diagram of GPIO 23 through a 220 ohm resistor to an LED anode with the cathode connected to GND, with the long and short legs labeled

Input: Reading a Button the Right Way

A button is just a switch: pressed it connects two points, released it connects nothing. The problem is the "nothing" state. A pin connected to nothing is floating, and a floating input picks up electrical noise and randomly reads HIGH and LOW. That is why a button wired with only one leg on a pin gives random values.

The cure is a pull resistor that holds the pin at a known level when the button is not pressed.

Pull-upPull-down
Resistor connects pin to3.3VGND
Button connects pin toGND3.3V
Released readsHIGHLOW
Pressed readsLOWHIGH

The ESP32 has internal pull-up and pull-down resistors (about 45 kΩ), so for a simple button you need no extra parts.

The easiest wiring: INPUT_PULLUP

Connect one leg of the button to GPIO 27 and the other leg to GND. That is all.

const int BUTTON_PIN = 27;

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
}

void loop() {
  if (digitalRead(BUTTON_PIN) == LOW) {
    Serial.println("Pressed");
  } else {
    Serial.println("Released");
  }
  delay(100);
}

The logic is inverted: pressed reads LOW. It feels backwards at first, and everybody trips on it once. This is the most common wiring in the Arduino world because it needs no external parts.

The exceptions: pins with no internal pull resistors

GPIO 34, 35, 36 and 39 have no internal pull-up or pull-down. A button on one of them needs a real resistor, typically 10 kΩ, from the pin to 3.3V (pull-up) or to GND (pull-down). Calling INPUT_PULLUP on those pins silently does nothing.

When to use an external resistor anyway

Use a real 10 kΩ resistor when wires are long (they act like antennas and pick up noise) or the signal is important. The internal 45 kΩ resistors are weak.

Image pending: Diagram comparing a pull-up button circuit and a pull-down button circuit, with the voltage level at the pin when pressed and released

Put Them Together: Button Controls LED

Light the LED while the button is held:

const int LED_PIN    = 23;
const int BUTTON_PIN = 27;

void setup() {
  pinMode(LED_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
}

void loop() {
  bool pressed = (digitalRead(BUTTON_PIN) == LOW);
  digitalWrite(LED_PIN, pressed ? HIGH : LOW);
}

That works because the LED simply mirrors the button. To toggle the LED with each press, you must detect the moment of the press, not just the state, and here a mechanical reality bites.

Debouncing: Why One Press Counts as Five

A mechanical button does not switch cleanly. For a few milliseconds the contacts bounce, producing a burst of rapid HIGH and LOW changes. The ESP32 is fast enough to see every one of them, so a naive "toggle on press" sketch may toggle several times per press and seem random.

The fix is to ignore changes that happen too soon after the previous one, without blocking the rest of your program:

const int LED_PIN    = 23;
const int BUTTON_PIN = 27;

bool ledState = false;
bool lastReading = HIGH;       // last raw reading
bool stableState = HIGH;       // debounced state
unsigned long lastChange = 0;
const unsigned long DEBOUNCE_MS = 40;

void setup() {
  pinMode(LED_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
}

void loop() {
  bool reading = digitalRead(BUTTON_PIN);

  if (reading != lastReading) {
    lastChange = millis();       // something changed, restart the timer
    lastReading = reading;
  }

  if (millis() - lastChange > DEBOUNCE_MS && reading != stableState) {
    stableState = reading;
    if (stableState == LOW) {    // a clean press
      ledState = !ledState;
      digitalWrite(LED_PIN, ledState ? HIGH : LOW);
    }
  }

  // other code can run here without being blocked
}

The button must hold a new value for 40 ms before it counts. Press once, the LED toggles once. Because the code uses millis() rather than delay(), the loop stays free for other work. Using delay(50) after a press also "works," but it freezes everything else while it waits, which breaks any project that does two things at once.

For instant reactions without checking in the loop, use interrupts, described in the ESP32 pinout guide. Debouncing still applies.

Image pending: Oscilloscope capture of a mechanical button press showing the contact bounce before the signal settles to LOW

Detecting Long Press, Short Press and Multiple Buttons

A common upgrade is to do different things for a short and a long press:

const int BUTTON_PIN = 27;
unsigned long pressStart = 0;
bool wasPressed = false;

void loop() {
  bool pressed = (digitalRead(BUTTON_PIN) == LOW);

  if (pressed && !wasPressed) {
    pressStart = millis();                 // press began
  }
  if (!pressed && wasPressed) {            // released: decide which kind it was
    unsigned long held = millis() - pressStart;
    if (held > 1500)      Serial.println("Long press");
    else if (held > 40)   Serial.println("Short press");
  }
  wasPressed = pressed;
}

For several buttons, keep their pins in an array and loop over them. Group per-button state in a small struct so each button has its own debounce timer.

const int BUTTONS[] = {27, 26, 25};
const int N = sizeof(BUTTONS) / sizeof(BUTTONS[0]);

void setup() {
  Serial.begin(115200);
  for (int i = 0; i < N; i++) pinMode(BUTTONS[i], INPUT_PULLUP);
}

void loop() {
  for (int i = 0; i < N; i++) {
    if (digitalRead(BUTTONS[i]) == LOW) {
      Serial.printf("Button %d pressed\n", i + 1);
      delay(200);                          // crude, fine for testing
    }
  }
}

Driving More Than an LED: Transistors, Relays and Buzzers

A GPIO is a signal source, not a power source. For anything that needs more than about 20 mA, let the pin switch a transistor and let the transistor carry the load.

NPN transistor (for example 2N2222 or BC547):

  • GPIO to the transistor base through a 1 kΩ resistor
  • Emitter to GND
  • Load (relay coil, buzzer, small motor) between the supply and the collector
  • A flyback diode across any coil or motor, cathode toward the supply, to absorb the voltage spike when it switches off
  • The load supply and the ESP32 share a ground

For heavier DC loads use a logic-level MOSFET such as the IRLZ44N. Relay modules include their own transistor stage, so you can connect the module's input directly to the ESP32 pin and power it from 5V, as shown in the smart switch guide. For smooth control of brightness or speed, drive the transistor with PWM instead of plain on and off, see the PWM tutorial.

Active-low outputs

Some modules, especially relay boards, turn on when the pin goes LOW. If your relay clicks the wrong way around, that is why. Define a constant for it so the logic is easy to flip:

const bool ACTIVE_LOW = true;
void setLoad(bool on) {
  digitalWrite(RELAY_PIN, (on != ACTIVE_LOW) ? HIGH : LOW);
}
Image pending: Circuit diagram of an ESP32 pin driving an NPN transistor with a base resistor, a relay coil and a flyback diode

Output State at Boot

When the ESP32 starts, pins are not instantly in the state your setup() chooses. For a moment they float or hold a default level. Usually that is invisible. On a relay or a motor driver it can cause a brief unwanted pulse.

  • For active-LOW relay boards, write HIGH first, then enable the output, so the relay never sees LOW: digitalWrite(pin, HIGH); pinMode(pin, OUTPUT);
  • Add a 10 kΩ resistor to hold the line at the safe level during the very first milliseconds of boot.
  • Avoid the strapping pins (GPIO 0, 2, 5, 12, 15) for loads that could hold them at the wrong level while booting. GPIO 12 held HIGH at power-up prevents some boards from starting at all.

Common Mistakes (and How to Spot Them)

Button reads random values. The pin is floating. Use INPUT_PULLUP, or add a resistor on GPIO 34 to 39.

Button seems to work backwards. With INPUT_PULLUP, pressed is LOW. Invert your condition.

LED does not light. Reversed polarity, no resistor in the circuit, wrong pin number or a broken jumper wire.

LED is very dim. Resistor too large, or a blue or white LED at 3.3V (use a smaller resistor).

Toggle fires several times per press. Bounce. Add the debounce logic above.

Board resets or acts strangely when the load switches on. The load is drawing power from the pin or the supply. Use a transistor and a proper supply, and add a flyback diode.

Upload fails after wiring something. A circuit on GPIO 0, 2 or the serial pins GPIO 1 and 3 interferes with flashing. Disconnect it during upload.

The pin gets hot or the ESP32 browns out. You are drawing too much current or shorting a pin. Disconnect and recheck.

5V sensor makes the pin unreliable. Use a level shifter or divider. Never connect 5V outputs to ESP32 inputs directly.

Image pending: Troubleshooting infographic showing a floating pin, an inverted pull-up button and a missing LED resistor with the fix for each

Reading Digital Sensors

The same digitalRead() works for any sensor with a digital output: a PIR motion sensor, a magnetic reed switch, an IR obstacle sensor, a flame sensor module or a tilt switch. Check each module's output voltage first. Most run from 3.3V or 5V, and a 5V module that outputs 5V needs a divider before it reaches the ESP32.

const int PIR_PIN = 13;

void setup() {
  Serial.begin(115200);
  pinMode(PIR_PIN, INPUT);       // the module drives the pin itself
}

void loop() {
  if (digitalRead(PIR_PIN) == HIGH) {
    Serial.println("Motion detected");
  }
  delay(200);
}

For values that are not just on or off, such as light or soil moisture, use analog input. See the ADC guide and remember to use ADC1 pins when WiFi is on.

Frequently Asked Questions

What is the difference between INPUT, INPUT_PULLUP and INPUT_PULLDOWN? INPUT leaves the pin floating, so you must supply the pull resistor yourself. The other two switch on an internal 45 kΩ resistor to 3.3V or GND.

Can I read and write the same pin? Yes, but change the mode with pinMode() first. A pin set as an output still reports its own state through digitalRead().

How many LEDs can I power from one ESP32? Each LED needs its own pin and resistor, around 10 mA each. The total current for the chip has a limit, so for more than a handful use a driver such as a shift register or a transistor array.

Can the ESP32 read a 5V button circuit? Only if the signal into the pin never exceeds 3.3V. With a normal button wired to GND and INPUT_PULLUP, there is no voltage problem at all.

What next? Add control from your phone with the WiFi tutorial and a web interface, or explore the ESP32 pinout guide for interrupts, touch and more.

Image pending: Finished breadboard with a debounced button toggling an LED and a PIR sensor reporting motion in the Serial Monitor

Signal In

Built from what makers search for

The lookups piling up in SolderHub's search bar every day shape what we publish next — new component pages, board guides, and firmware notes, sent out as they go live.

By subscribing you agree to our Privacy Policy. Unsubscribe anytime.