If you need to control hardware via URL parameters like ?led=1&state=on without freezing your ESP32's background tasks, the standard synchronous WebServer library will eventually cause watchdog resets under load. The direct solution is the ESPAsyncWebServer library paired with AsyncTCP. This combination offloads HTTP parsing to the lwIP (Lightweight IP) thread, keeping your main Arduino loop free for sensor polling and motor control.

Below is a complete, decision-forward guide to wiring, coding, and debugging an ESP32 async web server that safely extracts and acts on query parameters.

The Verdict: Sync vs. Async Web Servers on ESP32

Before writing a single line of code, you must choose the right server architecture. The ESP32's dual-core FreeRTOS environment handles concurrent connections poorly if the main thread blocks. Use this decision matrix to lock in your approach:

Criteria Standard WebServer (Sync) ESPAsyncWebServer (Async)
Concurrent Clients 1 (Blocks until response sent) Up to 8-12 (Non-blocking)
Serving Large Files (LittleFS) Fails / Causes WDT Resets Streams in chunks natively
RAM Overhead Low (~20KB) Higher (~45KB + TCP buffers)
Query Parameter Parsing server.arg() request->getParam()
Concrete Pick: If your project involves servos, stepper motors, or continuous sensor logging, choose ESPAsyncWebServer. The synchronous server will stall your hardware control loop while waiting for a browser to acknowledge an HTTP header. We proceed with the Async architecture for this build.

Parts List and Pin Mapping

This build targets the ubiquitous ESP32-WROOM-32 DevKit V1 (30-pin variant). We avoid GPIO 2 and GPIO 12 for outputs, as they are tied to boot-strapping resistors and can cause boot-loop failures if pulled high/low during power-on.

Component Specification / Variant Quantity
Microcontroller ESP32-WROOM-32 DevKit V1 (30-pin, Type-C or Micro-USB) 1
Indicators 5mm Standard LEDs (Red and Green) 2
Current Limiting 220Ω or 330Ω 1/4W Resistors 2
Prototyping 830-point Solderless Breadboard & Jumper Wires 1

Pin Mapping Table

ESP32 GPIO Component Notes
GPIO 16 Red LED (Anode via 220Ω) Safe for boot; no strapping conflicts.
GPIO 17 Green LED (Anode via 220Ω) Safe for boot; supports PWM if needed later.
GND LED Cathodes Common ground rail.

Complete ESP32 Async Web Server Query Parameters Example

The following code is fully compilable in the Arduino IDE (ESP32 core v2.0.14 or v3.x) or PlatformIO. It connects to WiFi, spins up the async server, and listens for two query parameters: target (which LED) and state (on/off).

Library Note (2026): The original me-no-dev ESPAsyncWebServer repository is unmaintained and fails to compile on ESP32 Arduino Core v3.x. Install the actively maintained fork by Mathieu Carbou via the Arduino Library Manager or PlatformIO registry.
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

// --- Network Credentials ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

// --- Pin Definitions ---
const int LED_RED = 16;
const int LED_GREEN = 17;

// --- Server Instance ---
AsyncWebServer server(80);

void setup() {
  Serial.begin(115200);
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_GREEN, OUTPUT);
  digitalWrite(LED_RED, LOW);
  digitalWrite(LED_GREEN, LOW);

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nConnected! IP address: " + WiFi.localIP().toString());

  // --- Route: Root ---
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/html", 
      "<h2>ESP32 Async Control</h2>" 
      "<p>Use: <code>/control?target=red&state=on</code></p>"
      "<p>Targets: red, green | States: on, off</p>");
  });

  // --- Route: Control via Query Parameters ---
  server.on("/control", HTTP_GET, [](AsyncWebServerRequest *request){
    
    // 1. Validate required parameters exist
    if (!request->hasParam("target") || !request->hasParam("state")) {
      request->send(400, "text/plain", "Error: Missing 'target' or 'state' parameter.");
      return;
    }

    // 2. Extract parameter values
    String target = request->getParam("target")->value();
    String state = request->getParam("state")->value();

    // 3. Map to hardware safely
    int pin = -1;
    if (target == "red") pin = LED_RED;
    else if (target == "green") pin = LED_GREEN;

    if (pin == -1) {
      request->send(400, "text/plain", "Error: Invalid target. Use 'red' or 'green'.");
      return;
    }

    // 4. Execute hardware state change
    if (state == "on") {
      digitalWrite(pin, HIGH);
      request->send(200, "text/plain", "Success: " + target + " LED turned ON.");
    } 
    else if (state == "off") {
      digitalWrite(pin, LOW);
      request->send(200, "text/plain", "Success: " + target + " LED turned OFF.");
    } 
    else {
      request->send(400, "text/plain", "Error: Invalid state. Use 'on' or 'off'.");
    }
  });

  // --- Handle 404 Not Found ---
  server.onNotFound([](AsyncWebServerRequest *request){
    request->send(404, "text/plain", "404: Endpoint not found.");
  });

  server.begin();
  Serial.println("Async HTTP server started.");
}

void loop() {
  // Keep loop empty or use for non-blocking sensor reads.
  // NEVER use delay() here when using AsyncTCP.
}

Debugging: First 3 Things to Check When It Crashes

AsyncTCP and ESPAsyncWebServer execute their callbacks inside the ESP32's lwIP (Lightweight IP) thread, not your main Arduino loop. If you violate the rules of this thread, the RTOS will panic. Here is the exact decision path for the three most common crash signatures.

1. The "Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1)"

  • The Cause: You placed a blocking function like delay(), Wire.requestFrom() without a timeout, or a long for loop inside the server.on() callback. The lwIP thread missed its watchdog feed.
  • The Fix: Never block in an async callback. If you need to trigger a slow hardware sequence (like moving a stepper motor), set a volatile bool flag inside the callback, and execute the physical movement inside the loop() based on that flag.

2. The "Exception (28): LoadProhibited" or "Stack Smashing Protect Failure"

  • The Cause: Heap fragmentation or out-of-bounds memory access. This usually happens when you heavily manipulate Arduino String objects inside the callback, or try to serve large HTML strings stored in RAM instead of Flash (PROGMEM/LittleFS).
  • The Fix: Use request->send_P() for large HTML payloads stored in flash. For query parameters, extract the value to a primitive type (like int or char array) immediately and discard the String object. Refer to the Espressif Watchdog API documentation for deeper RTOS thread context limits.

3. The "rst:0x10 (RTCWDT_RTC_RESET)" on Boot

  • The Cause: The WiFi connection loop in setup() is blocking the RTC watchdog because it lacks a yield() or delay() to feed the background RF tasks.
  • The Fix: Ensure your while (WiFi.status() != WL_CONNECTED) loop includes a delay(500); or yield(); inside it. (This is included in the provided code above).

Extending and Simplifying the Build

Once you have the baseline query parameter routing working, you will inevitably need to scale the project. Use this framework to decide your next architectural move:

Goal Implementation Strategy Complexity
Simplify UI Replace raw URL typing with a local HTML file hosted on LittleFS. Use JavaScript fetch() to send the query parameters in the background without reloading the page. Medium
Handle Complex Data Abandon URL query parameters for configuration. Switch to HTTP POST requests with a JSON body. Use ArduinoJson to parse the payload in the onRequestBody callback. High
Real-time Updates Implement WebSockets via AsyncWebSocket. Query parameters are strictly request-response; WebSockets allow the ESP32 to push sensor data to the browser instantly. High
Secure the Endpoint Add HTTP Basic Authentication. In the async callback, check request->authenticate("admin", "password") before parsing parameters. Return request->requestAuthentication() if it fails. Low
Pro-Tip for Production: If you deploy this ESP32 in an environment with spotty WiFi, add a WiFi reconnect handler. The WiFi.onEvent() callback can catch ARDUINO_EVENT_WIFI_STA_DISCONNECTED and trigger a reconnect without requiring a full hardware reboot, preserving your hardware states.

By keeping your async callbacks lean, validating parameters before mapping them to hardware, and respecting the lwIP thread boundaries, your ESP32 web server will run indefinitely without succumbing to the dreaded watchdog resets.