You press a button once, but your Arduino registers five or ten presses. This frustrating phenomenon is called button bounce, and it is one of the first real-world electronics problems every maker encounters. Understanding why it happens and how to fix it — in both hardware and software — is a fundamental skill that will make every Arduino project more reliable. This guide covers all the practical debounce techniques you need, with working code examples for each.
Table of Contents
- What Is Button Bounce and Why Does It Happen?
- Hardware Debounce Methods
- Software Debounce Methods
- Debouncing with Interrupts
- State Machine Approach
- Using the Bounce2 Library
- Which Method Should You Use?
- Frequently Asked Questions
What Is Button Bounce and Why Does It Happen?
When you press or release a physical button, the metal contacts inside do not make a clean, single connection. The contacts are made of springy metal that physically bounces against each other — making and breaking contact tens to hundreds of times in the span of 5 to 50 milliseconds before settling into a stable state. To human senses, this happens instantly. To an Arduino running at 16MHz and reading pins thousands of times per second, each contact bounce is a distinct press-and-release event.
Here is what actually happens on the oscilloscope when you press a button:
- Signal was HIGH (button released)
- Signal drops to LOW — first contact
- Signal bounces back to HIGH for 2ms
- Drops to LOW again for 1ms
- Bounces HIGH for 500µs
- Finally settles LOW — button fully pressed
The Arduino’s digital input detects each of these transitions. Without debouncing, one physical button press generates 3–10 input events. For a counter, LED toggle, or menu navigation this means your interface becomes completely unreliable.
Hardware Debounce Methods
Hardware debouncing solves the problem at the circuit level before the Arduino ever sees the signal. This is preferable for production designs because it requires zero CPU time and works regardless of what software is running.
Method 1: RC Low-Pass Filter
The simplest hardware debounce uses a resistor and capacitor to create a low-pass filter that smooths out the rapid bouncing transitions:
Components needed:
- 10kΩ resistor (pull-up or pull-down)
- 100nF to 100µF capacitor (10µF is a good starting point)
Circuit:
- Connect one side of the button to 5V
- Connect the other side to Arduino input pin AND one leg of capacitor
- Connect a 10kΩ resistor between the button output and GND (pull-down)
- Connect the other leg of capacitor to GND
The RC time constant (τ = R × C) determines how quickly the capacitor charges and discharges. With 10kΩ and 10µF, τ = 0.1 seconds — too slow for a responsive button. Use a smaller capacitor: 100nF gives τ = 1ms, which smooths bounce while still responding quickly enough to feel instant to the user.
The capacitor charges slowly enough that the bounce spikes do not cross the logic threshold, and the Arduino sees a clean transition. No code changes needed — just read digitalRead() normally.
Method 2: SR Latch with NAND Gates (CD4013 or 74HC00)
For the most reliable hardware debounce, use a Set-Reset (SR) latch made from two NAND gates (74HC00 IC). A double-pole button connects to both inputs. When pressed, the latch flips to a new stable state immediately and locks there — ignoring all subsequent bounce completely. This is the gold standard for hardware debounce and requires absolutely no timing assumptions.
This approach requires a SPDT (single-pole double-throw) or DPDT button with separate NO (normally open) and NC (normally closed) contacts.
Method 3: Schmitt Trigger Input
Arduino GPIO pins do not have built-in Schmitt trigger inputs on their digital pins. However, routing the button signal through a 74HC14 Schmitt trigger inverter before reaching the Arduino provides hysteresis — the input must cross a higher voltage to switch HIGH and a lower voltage to switch LOW, which eliminates noise-induced transitions. Combine with an RC filter for comprehensive hardware debounce without any software overhead.
Software Debounce Methods
Software debouncing is the most common approach in Arduino projects because it requires no extra components. The key is measuring time elapsed between state changes, not counting reads.
Method 1: Simple millis() Delay Debounce
This is the approach used in Arduino’s official Button tutorial and works reliably for most projects:
const int BUTTON_PIN = 2;
const int LED_PIN = 13;
const unsigned long DEBOUNCE_DELAY = 50; // milliseconds
int buttonState = LOW;
int lastButtonState = LOW;
unsigned long lastDebounceTime = 0;
int ledState = LOW;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP); // Internal pull-up
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
}
void loop() {
int reading = digitalRead(BUTTON_PIN);
// If reading changed, reset the debounce timer
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
// Only accept the reading if stable for DEBOUNCE_DELAY ms
if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
if (reading != buttonState) {
buttonState = reading;
// Only trigger on button PRESS (LOW because INPUT_PULLUP)
if (buttonState == LOW) {
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
Serial.println("Button pressed!");
}
}
}
lastButtonState = reading;
}
How it works: Every time the pin reading changes, the timer resets. Only when the reading has been stable for 50ms continuously does it count as a valid state change. During the bounce period (typically under 20ms), the reading keeps changing, which keeps resetting the timer. Once bouncing stops, 50ms passes with a stable reading and the press is registered exactly once.
Method 2: Edge Detection with Timestamps
A cleaner version that explicitly detects rising and falling edges:
const int BUTTON_PIN = 2;
const unsigned long DEBOUNCE_MS = 50;
bool buttonPressed = false;
bool lastStableState = HIGH; // With INPUT_PULLUP, released = HIGH
bool lastRawState = HIGH;
unsigned long lastChangeTime = 0;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
bool rawState = digitalRead(BUTTON_PIN);
unsigned long now = millis();
if (rawState != lastRawState) {
lastChangeTime = now; // State just changed — restart timer
lastRawState = rawState;
}
bool newStableState = lastStableState;
if ((now - lastChangeTime) > DEBOUNCE_MS) {
newStableState = rawState; // Signal stable for debounce period
}
// Detect falling edge (press)
if (lastStableState == HIGH && newStableState == LOW) {
Serial.println("Pressed");
buttonPressed = true;
}
// Detect rising edge (release)
if (lastStableState == LOW && newStableState == HIGH) {
Serial.println("Released");
buttonPressed = false;
}
lastStableState = newStableState;
}
Debouncing with Interrupts
If you need to detect button presses while the main loop is busy with other tasks (driving a display, communicating with sensors, etc.), hardware interrupts let the button interrupt the CPU immediately — but you still need debouncing to avoid fake triggers.
const int BUTTON_PIN = 2; // INT0 on Uno
const unsigned long DEBOUNCE_MS = 50;
volatile bool buttonEvent = false;
volatile unsigned long lastInterruptTime = 0;
void buttonISR() {
unsigned long now = millis();
// Ignore interrupts within DEBOUNCE_MS of last one
if (now - lastInterruptTime > DEBOUNCE_MS) {
buttonEvent = true;
lastInterruptTime = now;
}
}
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
Serial.begin(9600);
}
void loop() {
if (buttonEvent) {
buttonEvent = false; // Clear the flag
Serial.println("Interrupt: Button pressed!");
// Handle button press here...
}
// Other non-blocking tasks here
// (display updates, sensor readings, etc.)
}
Important notes for interrupt debouncing:
- Use
volatilefor any variables shared between ISR and main loop - Keep ISR code minimal — no Serial.print(), no delay() inside the ISR
- On Uno, hardware interrupts are only available on pins 2 (INT0) and 3 (INT1)
- On Mega, pins 2, 3, 18, 19, 20, 21 support interrupts
millis()inside an ISR is unreliable on some platforms — use it with caution
State Machine Approach
For the most robust button handling — including long-press detection and repeat-while-held behaviour — a state machine is the professional approach:
enum ButtonState { IDLE, DEBOUNCING_PRESS, PRESSED, DEBOUNCING_RELEASE };
const int BUTTON_PIN = 2;
const unsigned long DEBOUNCE_MS = 30;
const unsigned long LONG_PRESS_MS = 800;
ButtonState state = IDLE;
unsigned long stateStartTime = 0;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
bool isLow = (digitalRead(BUTTON_PIN) == LOW);
unsigned long now = millis();
switch (state) {
case IDLE:
if (isLow) {
state = DEBOUNCING_PRESS;
stateStartTime = now;
}
break;
case DEBOUNCING_PRESS:
if (!isLow) {
state = IDLE; // Noise — abandon
} else if (now - stateStartTime >= DEBOUNCE_MS) {
state = PRESSED;
Serial.println("Short press registered");
}
break;
case PRESSED:
if (!isLow) {
state = DEBOUNCING_RELEASE;
stateStartTime = now;
} else if (now - stateStartTime >= LONG_PRESS_MS) {
Serial.println("Long press detected!");
state = IDLE; // Handle long press action once
}
break;
case DEBOUNCING_RELEASE:
if (isLow) {
state = PRESSED; // Still pressed — go back
} else if (now - stateStartTime >= DEBOUNCE_MS) {
state = IDLE;
Serial.println("Button released");
}
break;
}
}
This state machine handles short presses, long presses, and release events separately — all without any blocking delays and with solid debouncing throughout.
Using the Bounce2 Library
If you want a battle-tested, ready-made solution, the Bounce2 library (available via Arduino Library Manager) wraps all the debounce logic into a clean API. Install it from Sketch → Include Library → Manage Libraries and search for “Bounce2”.
#include <Bounce2.h>
const int BUTTON_PIN = 2;
const int LED_PIN = 13;
Bounce2::Button button = Bounce2::Button();
int ledState = LOW;
void setup() {
button.attach(BUTTON_PIN, INPUT_PULLUP);
button.interval(25); // 25ms debounce interval
button.setPressedState(LOW); // LOW = pressed (INPUT_PULLUP)
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
}
void loop() {
button.update(); // Must call every loop iteration
if (button.pressed()) {
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
Serial.println("Pressed");
}
if (button.released()) {
Serial.println("Released");
}
}
Bounce2 is well-maintained, handles edge cases correctly, and supports multiple buttons without code repetition. For production projects with many buttons, it saves significant development time.
Which Method Should You Use?
Here is a practical guide to choosing the right debounce approach:
- Single button, simple project: Use the millis() software debounce. Easy to understand and modify.
- Multiple buttons: Use the Bounce2 library. Cleaner code, less repetition, proven reliability.
- Button in interrupt service routine: Combine hardware RC filter + software time check inside ISR.
- Long press + short press detection: Use the state machine approach.
- Production hardware / no CPU overhead acceptable: Use RC filter + Schmitt trigger for hardware debounce.
- Highest reliability, SPDT button available: Use SR latch hardware debounce.
The 50ms debounce time used in most examples is a reasonable default. For particularly bouncy switches or noisy environments, increase to 100ms. For high-speed applications where responsiveness matters, most quality tactile buttons settle in under 10ms, so 15–20ms may be sufficient.
Frequently Asked Questions
Why does my button sometimes register multiple presses even with a 50ms delay?
There are three common causes. First, your DEBOUNCE_DELAY may not be long enough — some cheap buttons bounce for up to 150ms. Try increasing it to 100ms. Second, you may have a wiring issue: floating inputs (no pull-up or pull-down resistor) pick up noise from the environment and generate false triggers. Always use INPUT_PULLUP or add a 10kΩ pull-down resistor. Third, if using interrupts, verify your ISR debounce check is comparing against millis() and not micros() by mistake.
What is the best debounce time value to use?
For most quality tactile buttons: 20–50ms works well. For membrane buttons or industrial pushbuttons: 50–100ms. For reed switches or mercury switches that can bounce for hundreds of milliseconds: use hardware debounce with an RC filter instead of a large software delay. Always measure with an oscilloscope or logic analyser if your button behaviour is unpredictable.
Does debouncing work the same way for INPUT_PULLUP and regular INPUT?
Yes, the logic is the same, but the active level is inverted. With INPUT_PULLUP, the pin reads HIGH when the button is released and LOW when pressed (button connects pin to GND). With regular INPUT and a pull-down resistor, the pin reads LOW when released and HIGH when pressed. All the code examples in this guide use INPUT_PULLUP — which is recommended because it uses the Arduino’s internal resistor and avoids floating inputs.
Can I debounce a rotary encoder the same way?
Rotary encoders need a different approach. They generate quadrature pulses that must be read in sequence, and a fixed time delay will cause you to miss steps. For encoders, hardware debounce (10nF capacitor on each CLK and DT line) is more effective, or use a dedicated encoder library like Encoder by Paul Stoffregen that handles the timing correctly.
How do I debounce a button connected to an analog pin?
Read the analog value with analogRead() and apply a threshold: if the reading is below 100 (approximately), consider the button pressed (for a pull-up configuration). Apply the same millis()-based time checking to the thresholded digital result. Analog inputs on Arduino do not have INPUT_PULLUP support, so add an external 10kΩ pull-up resistor between the pin and 5V.
Conclusion
Button debouncing is one of those foundational electronics concepts that separates unreliable prototypes from solid, production-quality projects. The software millis() method covers 90% of use cases with zero extra components. The Bounce2 library makes multi-button projects clean and maintainable. And for the 10% of cases where you need interrupt-driven or hardware-level debouncing, the approaches above give you the tools to handle them correctly.
Implement debouncing from the start of every project — retrofitting it later is always more painful than adding those five extra lines upfront.
Get your Arduino components from Zbotic.in’s Arduino collection — all genuine boards and accessories with fast shipping across India.
Add comment