Switch bounce is the rapid, unintended opening and closing of a mechanical switch's contacts for a few milliseconds after it changes state. It creates a burst of false transitions instead of one clean edge.Debouncing is any hardware or software technique that filters out those extra transitions, so the microcontroller registers exactly one event per press.
Bounce duration: from under 1 ms to roughly 20 ms for most switches, depending on the switch type, its age, and how hard it is pressed.
Typical software fix: accept a new button state only after it stays stable for about 20 ms.
Typical hardware fix: an RC filter feeding a Schmitt trigger input.
You press a push button once. Your microcontroller insists you pressed it five times. The counter jumps from 1 to 4, the LED flips when it should not, and the menu skips two screens.
Before you rewrite your logic, look at the switch. This is switch bouncing and debouncing, one of the first real-world problems every embedded developer meets, and the good news is that it is completely fixable.
What you will learn in this guide:
Switch bounce (also called contact bounce or contact chatter) is the rapid, unintended making and breaking of electrical contact when a mechanical switch opens or closes.
Think of dropping a basketball on the floor. It does not stop dead on the first touch. It rebounds, lands again, rebounds a little lower, and finally comes to rest. Switch contacts do the same thing, only thousands of times faster, and your microcontroller watches every single rebound.
Here is an active-low push button with a pull-up resistor (idle = HIGH, pressed = LOW). Instead of one clean falling edge, the pin flickers before it settles:
HIGH ---------+ +---+ +-+
| | | | |
LOW +-----+ +---+ +-------------------
^ ^
first contact settled
|<--- bounce -->|A single press can create anywhere from a couple of edges to dozens of them.
The burst usually lasts from under 1 ms to roughly 20 ms. Large, worn, or cheap switches can take longer.
The release bounces too, so the same problem appears when you let go.
Bench tip: Probe the button pin with a logic analyzer or oscilloscope and the bounce becomes visible in seconds. Capture a few hundred presses and note the longest burst.
A 16 MHz microcontroller runs about 16,000 clock cycles every millisecond.
A 5 ms bounce burst is therefore roughly 80,000 cycles of flickering input.
A fast polling loop or a GPIO interrupt sees every single flicker. Your code is not wrong. The MCU is simply much faster than the switch.
The effects of switch bounce range from annoying glitches to real reliability problems, because a microcontroller reacts to every edge it sees. Here is what goes wrong:
Run this experiment and you will see the effects of switch bounce on your own bench. Wire a push button between pin 2 and GND on an Arduino Uno, upload the sketch, and open the Serial Monitor:
volatile uint8_t edgeCount = 0;
void onEdge() {
edgeCount++; // count every falling edge the MCU sees
}
void setup() {
Serial.begin(115200);
pinMode(2, INPUT_PULLUP); // button between pin 2 and GND
attachInterrupt(digitalPinToInterrupt(2), onEdge, FALLING);
}
void loop() {
static uint8_t reported = 0;
if (edgeCount != reported) { // print whenever the count changes
reported = edgeCount;
Serial.println(reported);
}
}Press once and watch how far the number jumps. A clean press moves it by 1. A bouncy one jumps by 2, 3, or more.
The result changes from switch to switch and even from press to press.
Every extra count is a bounce edge that your code would have treated as a real press.
Switch debouncing is the process of filtering out contact bounce so that one physical press produces exactly one logical event. Every debounce method follows one rule: do not trust the first edge. Trust the signal only after it has stayed stable for longer than the bounce.
There are two families of solutions:
| Factor | Hardware Debouncing | Software Debouncing |
|---|---|---|
| Extra parts | Resistor, capacitor, gate, or IC | None |
| CPU load | Zero | Small (a timer tick or a few instructions) |
| Cost as button count grows | Rises with every button | Almost free |
| Flexibility | Fixed on the PCB | Timing changes in firmware |
| Best for | A few critical or interrupt-driven lines | Most products and prototypes |
An RC debounce circuit uses a capacitor to smooth out the flicker and a Schmitt trigger to turn the smoothed voltage into a clean digital edge.
Vcc
|
[R1] 10k pull-up
|
+-----------+-----------> to Schmitt trigger / MCU pin
| |
[R2] 330R [C] 1uF
| |
[SWITCH] |
| |
GND GNDPros: Cheap, needs no code, and works well on interrupt lines.
Cons: Adds delay to both edges, needs a Schmitt trigger input, and component tolerance shifts the timing.
An SR latch debouncer uses a single-pole double-throw (SPDT) switch and two cross-coupled NAND gates (a 74HC00, for example) to give a perfectly clean output with no added delay.
Chips such as the MAX6816, MAX6817, and MAX6818 (single, dual, and octal switch debouncers) and the MC14490 (hex contact bounce eliminator) handle the job in a single package.
They suit rugged panels with many inputs where you want zero firmware overhead.
Pro tip: Read your microcontroller’s datasheet before adding parts. Some devices include an input filter or debounce option inside the external interrupt block, and that costs nothing.
Software debouncing filters bounce in firmware, so it needs no extra components and the timing can be changed with one line of code.
The examples below assume an active-low button with a pull-up resistor. The functions gpio_read(), millis(), delay_ms(), and the BTN_PIN constant are placeholders for your platform’s equivalents, so the logic ports to Arduino, STM32 HAL, ESP-IDF, AVR, PIC, or 8051 toolchains.
bool button_pressed(void)
{
if (gpio_read(BTN_PIN) == LOW) { // possible press
delay_ms(20); // wait out the bounce
if (gpio_read(BTN_PIN) == LOW) { // still LOW? it is real
return true;
}
}
return false;
}Good for: Quick demos and first experiments.
Avoid in: Anything time-critical, because the CPU does nothing for 20 ms. Never use a delay inside an ISR. In an RTOS task, use the RTOS delay call instead.
Limitation: As written, it keeps returning true while the button is held, so track the release as well.
DEBOUNCE_MS.#define DEBOUNCE_MS 20
bool read_debounced(void)
{
static bool stable_state = false; // debounced result (true = pressed)
static bool last_raw = false; // previous raw reading
static uint32_t last_change = 0; // time of the last raw change
bool raw = (gpio_read(BTN_PIN) == LOW);
if (raw != last_raw) { // input moved, restart the timer
last_raw = raw;
last_change = millis();
}
if ((millis() - last_change) >= DEBOUNCE_MS) {
stable_state = raw; // stable long enough, accept it
}
return stable_state;
}Pro tip: The unsigned subtraction millis() - last_change stays correct even when the 32-bit millisecond counter rolls over (about every 49.7 days).
debounce_t per button in an array.#define STABLE_SAMPLES 4 // 4 samples x 5 ms tick = 20 ms
volatile bool button_event = false; // set here, cleared in the main loop
typedef struct {
uint8_t counter;
bool stable; // debounced state (true = pressed)
} debounce_t;
static bool debounce_update(debounce_t *d, bool raw)
{
if (raw == d->stable) {
d->counter = 0; // nothing pending
} else if (++d->counter >= STABLE_SAMPLES) {
d->stable = raw; // change held long enough
d->counter = 0;
}
return d->stable;
}
// Called every 5 ms from a timer interrupt or scheduler tick
void on_tick(void)
{
static debounce_t btn;
static bool was_pressed;
bool pressed = debounce_update(&btn, gpio_read(BTN_PIN) == LOW);
if (pressed && !was_pressed) {
button_event = true; // exactly one event per press
}
was_pressed = pressed;
}Keep the tick handler short: set a flag and do the real work in the main loop.
Shift-register variant: Push each sample into an 8-bit history with history = (history << 1) | pressed;, then treat 0xFF as stable pressed and 0x00 as stable released.
Polling every few milliseconds keeps the MCU awake. This method lets it sleep until the first edge arrives:
#define DEBOUNCE_MS 20
void button_isr(void) // fires on the first falling edge
{
gpio_irq_disable(BTN_PIN); // ignore the bounce that follows
timer_start_oneshot(DEBOUNCE_MS); // verify after the bounce window
}
void debounce_timer_isr(void) // fires once, about 20 ms later
{
if (gpio_read(BTN_PIN) == LOW) {
button_event = true; // confirmed press, flag for main loop
}
gpio_irq_clear_pending(BTN_PIN); // drop edges latched during the bounce
gpio_irq_enable(BTN_PIN);
}Do not skip the clear-pending step. Many MCUs latch the edge flag even while the interrupt is masked, so re-enabling without clearing can fire a false ISR immediately.
This version catches the press. To catch the release, re-arm for the rising edge and repeat the same logic.
| Your Situation | Best-Fit Method |
|---|---|
| Quick Arduino prototype | Timestamp (millis) debounce |
| Bare-metal product with several buttons | Timer-tick counter |
| Battery-powered IoT node that sleeps most of the time | Interrupt plus one-shot timer |
| One critical, interrupt-driven signal | RC filter plus Schmitt trigger |
| Clean signal from an SPDT switch in a logic circuit | SR latch |
| Long press, double click, or an RTOS-based UI | State machine in a task (IDLE, DEBOUNCE, PRESSED, RELEASE), fed by one of the methods above |
The right debounce time is longer than your switch’s worst-case bounce and shorter than your fastest real press.
Quick check: Your delay-based debounce waits 20 ms, but the switch keeps bouncing for 25 ms. What happens? The pin is read while it is still flickering, so you may read the wrong level and see a false or missed press. Measure the worst case and set the window safely above it.
volatile.Once you understand switch bouncing and debouncing, those phantom double presses finally make sense. The switch is not broken and your code is not wrong. A mechanical contact simply needs a few milliseconds to settle, and your firmware needs to wait for it.
Here is your next step: run the Arduino edge counter, watch the numbers jump, then apply Method 3 and see the count settle at exactly 1 per press. From now on, treat every mechanical input as noisy until it has proven otherwise.
Switch bounce is a mechanical switch rapidly connecting and disconnecting for a few milliseconds when it is pressed or released, instead of making one clean contact.
It causes false triggers, such as one press counted as several, random LED toggling, flooded interrupts, broken state machines, duplicate keypad characters, and extra wake-ups that drain the battery in IoT devices.
Typically from under 1 ms to roughly 20 ms. Large, worn, or cheap switches can take longer, so measure with a scope or logic analyzer.
Neither wins everywhere. Software is cheaper and flexible, which suits most designs. Hardware uses no CPU time, which suits a few critical or interrupt-driven inputs.
Yes. An interrupt fires on every edge, so bounce can trigger the ISR many times per press. Use an RC filter or the interrupt plus one-shot timer method.
Indian Institute of Embedded Systems – IIES