I Vibecoded a Blog Server on a $4 ESP32 with MicroPython and Microdot
I had an ESP32 sitting in a drawer. One of those $4 dev boards you buy with good intentions and forget about. This afternoon I plugged it in and started a Claude Code session:
> check if a esp32 is connected via usb and if yes figure out which one it is
> whats the best way to host a small webserver on it?
> lets setup micropython with microdot
That was the entire brief. Claude detected the chip, suggested MicroPython over Arduino/ESP-IDF for fast iteration, downloaded the latest firmware, flashed it, and set up the toolchain. Along the way I set up a dedicated IoT WiFi network for it, with MAC filtering. Claude presented me the ESP32’s MAC address in the terminal so I could whitelist it on my router.
Within minutes, it greeted me with this:
A live system dashboard served from a microcontroller. I got curious what else was possible. 37 lines of Python later, it serves a full styled blog with navigation, multiple pages, and live metrics.
The Stack
The whole thing runs on hardware most people wouldn’t trust with a blinking LED:
| Component | Spec |
|---|---|
| Chip | ESP32-D0WDQ6, dual-core 160MHz |
| RAM | 520KB total, ~114KB free for the app |
| Storage | 4MB flash |
| Network | WiFi 802.11 b/g/n |
| Cost | ~$4 |
| OS | MicroPython v1.28.0 |
| Web framework | Microdot v2.6.0 (single file, 59KB) |
The web server is main.py:
from microdot import Microdot, send_file
import gc, time, machine, network
app = Microdot()
@app.route('/')
def index(request):
return send_file('static/index.html')
@app.route('/static/<path:path>')
def static(request, path):
if '..' in path:
return 'Not found', 404
return send_file('static/' + path)
@app.route('/api/stats')
def stats(request):
gc.collect()
free = gc.mem_free()
used = gc.mem_alloc()
total = free + used
wlan = network.WLAN(network.STA_IF)
return {
'ram_free': free,
'ram_used': used,
'ram_total': total,
'ram_pct': (used * 100) // total,
'cpu_mhz': machine.freq() // 1000000,
'uptime': time.ticks_ms() // 1000,
'ip': wlan.ifconfig()[0],
}
app.run(port=80)
That’s the entire backend. Static file serving with a basic path traversal check (though I wouldn’t trust '..' in path in production) and a JSON API that returns live system metrics. The frontend is vanilla HTML/CSS/JS with a dark theme, three blog posts, a dashboard page, and a stats bar that polls /api/stats every few seconds.
The Vibecoding Process
This was a single session of directing Claude Code. I described what I wanted, it flashed MicroPython onto the ESP32, set up Microdot, scaffolded the blog, added navigation, built the dashboard, and ran load tests. I steered the direction, it handled the implementation.


How Fast Is It
I ran four rounds of load tests to answer the obvious question: can a $4 chip actually serve web pages reliably?
| Test | Requests | Success | Avg Response | Throughput |
|---|---|---|---|---|
| Sequential (HTML) | 20 | 100% | 0.198s | 5.1 req/s |
| Sequential (API) | 20 | 100% | 0.216s | 4.6 req/s |
| 5 concurrent | 5 | 100% | 0.801s | 6.2 req/s |
| 10 concurrent | 10 | 100% | 1.135s | 8.8 req/s |
| 20 concurrent | 20 | 90% | 1.725s | 10.4 req/s |
| Sustained (50 in batches of 5) | 50 | 100% | 0.655s | 7.6 req/s |
200ms per request. Consistently. Across four test runs, on different WiFi access points, with different page sizes. The ESP32 doesn’t care. Sequential average stayed between 0.198s and 0.201s every single time.
The hard limit is 20 concurrent connections. Exactly 2 get refused every time, which matches Microdot’s listen(5) socket backlog. Not a crash, not a memory issue. Just a queue that’s full.
The sustained load test is the most telling: 50 requests in batches of 5, 100% success rate, no memory leaks, no degradation. The chip is rock solid.
What Surprised Me
WiFi latency doesn’t matter as much as you’d think. I tested from both a distant and a close access point. Sequential throughput was identical (0.198s). The ESP32’s request handling time dominates over network latency.
Page size is irrelevant at this scale. 1,557 bytes vs 1,980 bytes made less than 2% difference in response time. At these sizes, the overhead is in the TCP handshake and Python execution, not data transfer.
114KB of free RAM is plenty. The MicroPython runtime, Microdot, all static files, and the running server fit comfortably. Garbage collection keeps it stable across hundreds of requests. The constraint that I expected to hit never materialized.
Why This Matters
This connects to something I’ve been thinking about with AI adoption. The barrier to building things keeps dropping. Not because the hardware got better (the ESP32 has been around since 2016) but because directing an AI agent to “make this chip serve a website” is now a realistic afternoon project.
A decade ago, getting a microcontroller to serve HTTP required deep embedded knowledge: cross-compilation, memory management, RTOS configuration. Today it’s 37 lines of Python on a $4 chip, built in a conversation. The stack that makes this possible (MicroPython + Microdot) has existed for years. What changed is that an AI agent can wire it all together without you needing to know the details upfront.
The ESP32 blog is a toy. But the pattern is real: describe what you want, let the agent figure out the implementation, verify the result. That’s the same loop whether you’re configuring a 3D printer or benchmarking a web server on a chip the size of a postage stamp.
The views and opinions expressed here are my own and do not reflect those of my employer.