You've seen it on Reddit — someone mounted an ESP32 on their wall and it's serving a live public website. Your first reaction is probably "that's cool but wildly impractical." Your second reaction, if you're anything like me, is "I need to try this immediately."
I did. And it crashed. A lot. Here's everything I learned getting a stable, publicly accessible website running on a microcontroller that costs less than lunch.
The Core Problem: ESP32 Wasn't Built for This
The ESP32 has roughly 520KB of SRAM and around 4MB of flash storage depending on your board. For context, the HTML of a typical modern webpage is often larger than the ESP32's entire working memory. Serving even a single concurrent request can eat up most of your available heap.
The real issues I kept hitting:
- Random crashes after a few hours of uptime
- Connection timeouts when more than 2-3 people hit the site simultaneously
- No way for the public internet to reach the device behind my home router
- Memory fragmentation slowly eating away at available heap until the watchdog triggers a reboot
Let's fix each one.
Step 1: Set Up a Lean Web Server
Forget serving anything dynamic at first. The ESP32's built-in HTTP server in the ESP-IDF framework (or the Arduino WebServer library) works, but you need to be surgical about memory.
#include <WiFi.h>
#include <WebServer.h>
WebServer server(80);
// Store pages in PROGMEM (flash) instead of eating RAM
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head><title>Wall Server</title></head>
<body>
<h1>Served from an ESP32 on my wall</h1>
<p>Free heap: %HEAP% bytes</p>
</body>
</html>
)rawliteral";
void handleRoot() {
String html = String(index_html);
// Show remaining memory — useful for debugging
html.replace("%HEAP%", String(ESP.getFreeHeap()));
server.send(200, "text/html", html);
}
void setup() {
WiFi.begin("your_ssid", "your_password");
while (WiFi.status() != WL_CONNECTED) delay(500);
server.on("/", handleRoot);
server.begin();
}
void loop() {
server.handleClient();
delay(2); // small yield to prevent watchdog resets
}
The key detail here is PROGMEM. If you allocate your HTML as regular strings, they live in RAM. On a device with 520KB total, that matters fast. PROGMEM keeps them in flash, which you have a lot more of.
Step 2: Solve the Crash Loop
My ESP32 would run for about 3-4 hours and then reboot. The culprit was memory fragmentation from String operations. Every time you concatenate or replace inside an Arduino String, it allocates and frees heap memory. Over hours, the heap turns into Swiss cheese.
The fix is a combination of a watchdog pattern and avoiding String where possible:
// Use snprintf with a stack buffer instead of String concatenation
void handleRoot() {
char buf[1024]; // fixed stack allocation, no fragmentation
snprintf(buf, sizeof(buf),
"<!DOCTYPE html><html><head><title>Wall Server</title></head>"
"<body><h1>Served from an ESP32</h1>"
"<p>Uptime: %lu seconds</p>"
"<p>Free heap: %u bytes</p></body></html>",
millis() / 1000,
ESP.getFreeHeap()
);
server.send(200, "text/html", buf);
}
// In loop(), add a heap monitor that reboots before crashing
void loop() {
server.handleClient();
// If heap drops dangerously low, do a controlled restart
if (ESP.getFreeHeap() < 20000) {
ESP.restart(); // graceful reboot beats a hard crash
}
delay(2);
}
After switching from String to snprintf, my uptime went from 4 hours to weeks. The heap guard is a safety net — if something else leaks memory, you get a clean reboot instead of a hard fault.
Step 3: Expose It to the Public Internet
This is where most people get stuck. Your ESP32 is sitting behind your home router with a private IP. You've got a few options:
- Port forwarding — works but exposes your home IP, and many ISPs use CGNAT now which makes this impossible
- Dynamic DNS + port forward — same problem, slightly more convenient
- Reverse tunnel — the actual answer
I use a reverse tunnel approach with a cheap VPS. The idea: your ESP32 doesn't accept incoming connections directly. Instead, a small relay on a VPS forwards traffic to your device.
But the ESP32 can't run SSH tunnels directly. So you need a lightweight relay. Here's the architecture:
[Public Internet] → [VPS with nginx] → [WebSocket tunnel] → [ESP32 on your wall]
On the ESP32 side, maintain a persistent WebSocket connection to your VPS. On the VPS, nginx accepts public HTTP requests and forwards them through the WebSocket to the ESP32. Tools like Cloudflare Tunnel can simplify this — the ESP32 connects outbound to Cloudflare, and they route public traffic back through that connection. There are also lightweight options like frp (fast reverse proxy) where you run the server component on your VPS.
Alternatively, if your ISP gives you a real public IP, the dead-simple approach works:
- Set a static IP for the ESP32 on your router
- Forward port 80 (and 443 if you handle TLS) to that IP
- Use a free dynamic DNS service to point a domain at your home IP
I'd strongly recommend the tunnel approach though. Don't expose your home network directly if you can avoid it.
Step 4: Handle the Traffic Spike
When someone posts your wall-mounted server to Reddit, you'll get a few hundred concurrent visitors. The ESP32 handles maybe 4-5 simultaneous TCP connections before it starts dropping requests.
The solution is to put your VPS to work. Configure nginx on the relay to cache responses:
proxy_cache_path /tmp/esp_cache levels=1:2 keys_zone=esp:1m max_size=10m inactive=60s;
server {
listen 80;
server_name your-esp-site.example.com;
location / {
proxy_pass http://esp32_backend;
proxy_cache esp;
proxy_cache_valid 200 30s; # cache responses for 30 seconds
proxy_cache_use_stale error timeout updating;
}
}
This way the ESP32 only gets hit once every 30 seconds regardless of traffic. The VPS handles the actual load. Your microcontroller just needs to respond to one request every half minute — completely doable.
Prevention Tips: Keep It Running Long-Term
- Use the ESP32's hardware watchdog timer — if your code hangs, the hardware reboots it automatically. The ESP-IDF configures this by default; don't disable it.
-
Monitor heap over time — log
ESP.getFreeHeap()on every request. If you see a downward trend, you have a leak. -
Avoid the Arduino
Stringclass in anything long-running. Use fixed buffers. -
Implement OTA updates — the
ArduinoOTAlibrary lets you push firmware updates over WiFi. You do not want to pull the board off the wall every time you change the HTML. -
Add a
/healthendpoint that returns uptime and heap stats. Point an external uptime monitor at it so you know when it goes down. - Use a board with an external antenna if it's mounted inside a wall or in a spot with weak WiFi. The onboard PCB antenna is decent but not great through drywall.
Is This Practical?
Honestly? Not really. A $5/month VPS will serve your personal site better in every measurable way. But that's not the point.
Running a website on an ESP32 teaches you things about memory management, networking, and system reliability that you'd never learn from deploying to a cloud provider. When your entire server has less RAM than a single Node.js process uses at idle, every byte matters. Every allocation matters. Every connection matters.
Plus, there's something deeply satisfying about pointing at a $10 board on your wall and saying "that's my web server." Try it. Just maybe don't put it in production.
Top comments (0)