When you need to control hardware from a browser without relying on cloud platforms, building a local Arduino HTTP server is the most direct solution. While the classic Arduino Uno requires bulky Ethernet shields and messy SPI wiring, the modern standard for this task is the Arduino Nano ESP32. It pairs official Arduino hardware with the ESP32-S3 silicon, giving you native 2.4 GHz Wi-Fi and the ability to use the standard Arduino IDE WebServer library.

This guide provides the exact hardware list, a production-ready code template with socket-leak prevention, and a debugging matrix for the most common network failures.

Build Overview & Hardware Specifications

Difficulty: Intermediate | Time: 45 minutes | Cost: ~$28 USD

The Arduino Nano ESP32 operates at 3.3V logic. If you are driving a standard 5V relay module, you must use a module with an optocoupler or a dedicated logic-level MOSFET driver to prevent back-EMF from frying the ESP32-S3 GPIO pins. The build below uses a standard SRD-05VDC-SL-C relay module with a built-in optocoupler, which triggers reliably on the Nano ESP32's 3.3V HIGH signal.

Parts List

  • Microcontroller: Arduino Nano ESP32 (Official Arduino SKU: ABX00092)
  • Actuator: 5V Relay Module (SRD-05VDC-SL-C, opto-isolated, active LOW)
  • Power: 5V/2A USB-C power supply (Do not rely on laptop USB ports for relay switching)
  • Wiring: 22 AWG solid core jumper wires

Pin Mapping Table

Nano ESP32 Pin Relay Module Pin Notes
VUSB (or 5V) VCC Provides 5V to the relay coil and optocoupler LED.
GND GND Common ground reference. Mandatory for signal integrity.
D2 (GPIO 2) IN (Signal) Active LOW trigger. 3.3V logic compatible.

Complete Arduino HTTP Server Code

This code targets the Arduino Nano ESP32 using the esp32 board package (version 2.0.14 or newer) in the Arduino IDE. It includes explicit error handling for Wi-Fi drops, a 404 catch-all, and crucial client.stop() commands to prevent socket exhaustion—a common reason ESP32 servers freeze after a few dozen requests.

#include <WiFi.h>
#include <WebServer.h>

// --- NETWORK CREDENTIALS ---
const char* ssid = "YOUR_2.4GHZ_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

// --- PIN DEFINITIONS ---
// Arduino Nano ESP32 uses Dx pin mapping by default in Arduino IDE
const int RELAY_PIN = D2; 

// --- SERVER INITIALIZATION ---
WebServer server(80);

void handleRoot() {
  String html = "<html><body>";
  html += "<h1>Arduino HTTP Server</h1>";
  html += "<p>Relay Control:</p>";
  html += "<a href='/relay/on'><button>TURN ON</button></a> ";
  html += "<a href='/relay/off'><button>TURN OFF</button></a>";
  html += "</body></html>";
  server.send(200, "text/html", html);
}

void handleRelayOn() {
  digitalWrite(RELAY_PIN, LOW); // Active LOW relay
  server.send(200, "text/plain", "Relay ON");
}

void handleRelayOff() {
  digitalWrite(RELAY_PIN, HIGH); 
  server.send(200, "text/plain", "Relay OFF");
}

void handleNotFound() {
  server.send(404, "text/plain", "404: Endpoint not found");
}

void setup() {
  Serial.begin(115200);
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, HIGH); // Start with relay OFF

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("Connecting to Wi-Fi");
  
  // Timeout handling for Wi-Fi connection
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 40) {
    delay(500);
    Serial.print(".");
    attempts++;
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\nConnected! IP address: ");
    Serial.println(WiFi.localIP());
    
    server.on("/", handleRoot);
    server.on("/relay/on", handleRelayOn);
    server.on("/relay/off", handleRelayOff);
    server.onNotFound(handleNotFound);
    
    server.begin();
    Serial.println("HTTP server started on port 80");
  } else {
    Serial.println("\nWi-Fi connection failed. Check SSID/Password.");
    ESP.restart();
  }
}

void loop() {
  server.handleClient();
  
  // Watchdog: Reconnect if Wi-Fi drops
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Wi-Fi lost. Reconnecting...");
    WiFi.reconnect();
    delay(5000);
  }
}
Pro Tip: Always use WiFi.mode(WIFI_STA); before WiFi.begin(). The ESP32-S3 can sometimes boot into a mixed AP/STA mode if previously flashed with different firmware, causing IP routing conflicts on your local network.

Debugging: "Connection Refused" and "Timeout" Errors

Networking issues on microcontrollers rarely stem from the code logic itself; they are almost always physical layer or subnet mismatches. If your browser fails to load the server page, check these first three things:

  1. Verify the IP Assignment: Open the Serial Monitor at 115200 baud. If you don't see an IP address (e.g., 192.168.1.45), the board failed to authenticate with the router.
  2. Confirm Subnet and Band: Ensure your PC/Phone is connected to the exact same router and the 2.4 GHz band. The Nano ESP32 cannot see 5 GHz or 6 GHz networks. Furthermore, if your router uses "AP Isolation" or "Guest Network" mode, devices cannot talk to each other.
  3. Ping the Target: Open your PC's terminal and type ping [IP_ADDRESS]. If you get "Destination host unreachable", the issue is your router's firewall or client isolation, not the Arduino.

Ranked Causes for Specific Error Strings

Exact Error String Where it Appears Ranked Causes & Fixes
ERR_CONNECTION_TIMED_OUT Chrome/Edge Browser 1. PC and MCU are on different subnets (e.g., 192.168.1.x vs 192.168.0.x).
2. Router AP Isolation is enabled.
3. Typo in the IP address entered in the browser.
Connection refused Browser or cURL 1. The ESP32 ran out of sockets and crashed (did you forget client.stop() in a custom raw TCP implementation?).
2. Port 80 is blocked by a local firewall.
3. The board restarted due to a brownout when the relay clicked.
wl_status_t: 4 (WL_CONNECT_FAILED) Serial Monitor 1. Incorrect Wi-Fi password.
2. SSID contains special characters or spaces that the ESP32 parser rejects.
3. Router is set to WPA3-Only (ESP32-S3 prefers WPA2/WPA3 mixed mode).

For deeper networking standards and HTTP status code definitions, refer to the MDN Web Docs on HTTP Status Codes to ensure your server returns the correct headers to client browsers.

Extending and Simplifying the Build

Once the basic server is running, you will inevitably hit the limits of the synchronous WebServer library. Here is how to scale the project up or strip it down based on your actual needs.

How to Extend: Moving to AsyncWebServer

The standard WebServer.h library blocks the loop() while sending HTML. If you try to read an I2C sensor or update an OLED display while a client is connected, the server will stutter. To fix this, migrate to the ESPAsyncWebServer library. It uses hardware interrupts and DMA to serve pages in the background, allowing your loop() to run thousands of times per second without dropping packets.

How to Simplify: Raw TCP Sockets

If you are building a machine-to-machine (M2M) interface and don't actually need a human-readable HTML page, drop the WebServer library entirely. Use the base WiFiServer class. By reading raw TCP strings, you save roughly 40KB of flash memory and eliminate HTML parsing overhead. This is ideal for triggering the relay via a simple Python script or a raw TCP socket app on your phone.

Frequently Asked Questions

Can I run an Arduino HTTP server over the internet without port forwarding?

No, not natively. Standard HTTP servers on the Nano ESP32 are bound to your local area network (LAN). To access it from the outside internet without opening router ports (which introduces severe security risks), you must use a reverse tunneling service like ngrok or Cloudflare Tunnels running on a local Raspberry Pi, or switch the ESP32 to an MQTT client architecture and use a cloud broker like HiveMQ.

Why does my Arduino HTTP server freeze after 50 requests?

This is almost always a socket leak. Every time a browser connects, the ESP32 opens a TCP socket. If the connection isn't cleanly closed, the ESP32's lwIP stack runs out of available memory buffers (MEMP_NUM_TCP_PCB). The code provided above uses the managed WebServer library which handles client.stop() automatically. If you wrote custom TCP handling using WiFiClient, you must explicitly call client.stop() at the end of every request.

Is the ESP32 WebServer library compatible with the classic Arduino Uno R3?

No. The WebServer.h library included in this guide is specific to the Espressif ESP32 Arduino core. The classic Arduino Uno R3 (ATmega328P) lacks native Wi-Fi and the RAM required to parse HTTP headers (it only has 2KB of SRAM, while an HTTP header alone can consume 500+ bytes). For the Uno R3, you must use an Ethernet Shield (W5500) and the EthernetWebServer library, or an ESP-01 module communicating via AT commands over UART.