If you are wiring a mechanical switch to a microcontroller, the default choice for arduino button debounce in 2026 is the Bounce2 library with a 10-millisecond polling interval on an Arduino Nano v3. Skip the hardware RC (resistor-capacitor) filters unless you are strictly bound to pin-change interrupts where software polling is impossible. Mechanical contacts do not close cleanly; they chatter. Without debounce, a single button press can register as a dozen rapid triggers, completely breaking your state machine or incrementing a counter out of control.

The Direct Answer: Use the Bounce2 library. Set the interval to 10 (10ms) for standard 6x6mm tactile switches, and use the microcontroller's internal INPUT_PULLUP resistor to eliminate the need for external pull-down resistors.

The Physics of Contact Bounce: Why Your Code Sees Ghosts

When the metal contacts inside a tactile switch or toggle switch physically collide, they do not mate perfectly on the first impact. The microscopic irregularities on the contact surfaces cause them to bounce apart and slam back together multiple times before settling. According to Jack Ganssle's definitive Guide to Debouncing, a typical switch will bounce for anywhere from 1 to 5 milliseconds, though some heavily worn or low-cost switches can chatter for up to 20ms.

Because an Arduino Nano running at 16 MHz can execute millions of instructions per second, a 5ms bounce window is an eternity. The microcontroller reads the pin transitioning from HIGH to LOW dozens of times during that single physical press. If your code simply checks if (digitalRead(BUTTON_PIN) == LOW), your logic will execute repeatedly, leading to erratic behavior, skipped menu items, or double-firing relays.

Decision Tree: Hardware vs. Software Debounce

Choosing the right debounce method depends entirely on your project's architecture. Use this decision matrix to terminate on the correct approach for your specific build.

Criteria Software Delay (delay()) Software Library (Bounce2) Hardware RC Filter (10kΩ + 100nF)
Blocking? Yes (halts CPU) No (non-blocking) No (analog filtering)
Interrupt Safe? No No (requires polling) Yes (cleans signal pre-ISR)
Component Count 0 extra 0 extra +2 per button
Code Complexity Low (but bad practice) Medium (state machine) Low (hardware does the work)
The Concrete Pick: For 95% of hobbyist and commercial polling loops, choose the Bounce2 library. It is non-blocking, requires no extra BOM components, and handles edge detection (fell() and rose()) automatically. Only choose the Hardware RC filter if you are wiring the button directly to a hardware interrupt pin (like D2 or D3 on the Nano) and cannot afford the latency of a software polling loop.

Parts List & Pin Mapping (Arduino Nano v3)

This build targets the Arduino Nano v3 (ATmega328P). It is the standard breadboard-friendly variant. The code relies on the internal pull-up resistors, meaning we wire the button to ground, not VCC.

Bill of Materials

  • Microcontroller: Arduino Nano v3 (ATmega328P, 16MHz)
  • Switch: 6x6mm Through-hole Tactile Pushbutton (e.g., Omron B3F-1000)
  • Indicator: 5mm LED with 220Ω current-limiting resistor
  • Wiring: 22 AWG solid core hook-up wire

Pin Mapping Table

Component Nano Pin Mode Notes
Tactile Button (Leg 1) D2 INPUT_PULLUP Internal 20kΩ-50kΩ pull-up enabled
Tactile Button (Leg 2) GND Reference Completes the circuit to ground
LED Anode (+) D13 OUTPUT Includes 220Ω resistor in series
LED Cathode (-) GND Reference Common ground rail

The Bounce2 Implementation: Complete Code

The following code is fully compilable. It uses the Bounce2 library to track state changes without blocking the main loop. It also includes a heartbeat LED indicator on pin D12 to prove the loop is running and not locked up, which is a critical debugging feature for embedded systems.

#include <Bounce2.h>

// --- PIN DEFINITIONS ---
const int BUTTON_PIN = 2;    // Tactile switch wired to GND
const int LED_PIN = 13;      // Main indicator LED
const int HEARTBEAT_PIN = 12; // Loop health indicator

// --- DEBOUNCE CONFIGURATION ---
// 10ms is the standard safe interval for most tactile switches.
// If using large mechanical toggle switches, increase to 20-50ms.
const unsigned long DEBOUNCE_INTERVAL = 10; 

// Instantiate the Bounce object
Bounce debouncer = Bounce();

// State tracking
bool ledState = false;
unsigned long previousHeartbeat = 0;
const long heartbeatInterval = 500; // 500ms blink for heartbeat

void setup() {
  // Initialize Serial for debugging
  Serial.begin(115200);
  while (!Serial && millis() < 2000) {
    // Wait for serial port to connect (max 2 seconds)
  }
  Serial.println(F("System Boot: Debounce Demo Initialized"));

  // Configure Pins
  pinMode(LED_PIN, OUTPUT);
  pinMode(HEARTBEAT_PIN, OUTPUT);
  
  // INPUT_PULLUP activates the internal ~30k resistor.
  // The pin reads HIGH when open, LOW when pressed.
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  // Attach the debouncer to the pin and set the interval
  debouncer.attach(BUTTON_PIN);
  debouncer.interval(DEBOUNCE_INTERVAL);

  // Initial state sync
  digitalWrite(LED_PIN, ledState);
}

void loop() {
  // 1. Update the debouncer (MUST be called every loop iteration)
  debouncer.update();

  // 2. Check for a button press (transition from HIGH to LOW)
  if (debouncer.fell()) {
    ledState = !ledState; // Toggle state
    digitalWrite(LED_PIN, ledState);
    
    Serial.print(F("Button Pressed. Duration: "));
    Serial.print(debouncer.previousDuration());
    Serial.println(F("ms"));
  }

  // 3. Check for button release (optional, useful for hold-logic)
  if (debouncer.rose()) {
    Serial.println(F("Button Released."));
  }

  // 4. Heartbeat LED (Non-blocking error handling/health check)
  unsigned long currentMillis = millis();
  if (currentMillis - previousHeartbeat >= heartbeatInterval) {
    previousHeartbeat = currentMillis;
    digitalWrite(HEARTBEAT_PIN, !digitalRead(HEARTBEAT_PIN));
  }
}

Troubleshooting: When Your Button Still Chatters

Even with the right library, physical wiring and configuration errors can mimic bounce. If your system is misbehaving, follow this diagnostic path.

The First Three Things to Check

  1. Verify the Common Ground: If your button is on a separate breadboard power rail, ensure the ground rail is physically jumpered to the Nano's GND pin. A floating ground will cause the internal pull-up to read random electromagnetic noise as button presses.
  2. Check the Pull-Up Configuration: If you wired the button to VCC (5V) instead of GND, INPUT_PULLUP will not work. You must either rewire to GND or change the code to INPUT and add an external 10kΩ pull-down resistor.
  3. Audit the Interval Value: If you are using heavy-duty arcade buttons or industrial limit switches, 10ms is too short. Increase debouncer.interval(50) to accommodate the heavier metal contacts.

Compiler Error: fatal error: Bounce2.h: No such file or directory

This is the most common roadblock for beginners. The Arduino IDE cannot find the library. Here are the ranked causes and fixes:

  • Cause 1 (Most Likely): The library is not installed. Fix: Go to Sketch > Include Library > Manage Libraries, search for 'Bounce2' by Thomas Ouellet Fredericks, and click Install.
  • Cause 2: Typo in the include statement. Fix: Ensure you typed #include <Bounce2.h> (capital B, number 2). The older, deprecated library was just Bounce.h, which lacks the .fell() and .rose() methods used in modern code.
  • Cause 3: Corrupted IDE cache. Fix: Close the IDE, navigate to your Documents/Arduino/libraries folder, delete the Bounce2 folder, restart the IDE, and reinstall via the Library Manager.

Extending and Simplifying the Build

Once you have a single button debounced reliably, you will inevitably need to scale the interface. Here is how to adapt the architecture without rewriting your core logic.

Scaling Up: Keypad Matrices

If you need more than 4 or 5 buttons, do not wire each to a dedicated GPIO pin; you will run out of pins on the Nano. Instead, use a resistor-diode matrix or a dedicated I2C GPIO expander like the MCP23017. The MCP23017 gives you 16 extra interrupt-capable pins over just two I2C wires. You can attach a Bounce2 instance to each of the 16 expander pins by reading the I2C register states into a local byte array, then feeding those bytes into the debouncer's software logic.

Simplifying: The Hardware RC Fallback

If you are building a simple circuit on a perfboard and want to eliminate the library dependency entirely to save flash memory (the Nano has 32KB, but every byte counts in ultra-compact ATTiny85 builds), you can use a hardware RC filter. Wire a 10kΩ resistor in series with the switch, and place a 100nF ceramic capacitor from the microcontroller pin to ground. This creates a low-pass filter with a time constant (τ = R × C) of 1ms, physically smoothing out the voltage spikes before they ever reach the silicon. However, remember that hardware filters introduce a slight analog ramp-up time, which can cause undefined logic states if the microcontroller samples the pin exactly during the capacitor's charge curve. For pure digital reliability, stick to the Bounce2 library.

By standardizing on the Bounce2 library and INPUT_PULLUP wiring, you eliminate 90% of the ghost-triggering issues that plague embedded projects. Keep your intervals tuned to your specific switch hardware, and always include a heartbeat LED to verify your main loop is executing cleanly.