If you want to build a lightweight, low-power IoT node, hosting a native web server for Raspberry Pi Pico W using MicroPython is the most efficient path. Unlike spinning up a heavy Flask or FastAPI environment on a full Raspberry Pi 4 or 5, the Pico W handles HTTP requests directly via bare-metal sockets. This approach uses less than 40KB of RAM, boots in under two seconds, and allows you to toggle GPIO pins and read I2C sensors without an underlying OS getting in the way.

This guide targets the Raspberry Pi Pico W (RP2040 with CYW43439 Wi-Fi) running MicroPython v1.22+. We will wire a BME280 environmental sensor and a 3.3V relay module, write a non-blocking socket server with hardware watchdog recovery, and troubleshoot the exact error strings that trip up most embedded developers.

Project Spec Sheet & Hardware BOM

Difficulty Rating: Intermediate (Requires basic I2C debugging and MicroPython REPL familiarity).
Time to Build: 45 minutes.

The biggest mistake makers make when building a Raspberry Pi Pico W web server is pairing it with standard 5V Arduino relay modules. The Pico W GPIO pins output 3.3V and are not 5V tolerant. Driving a standard optocoupler relay directly from a Pico GPIO will often fail to trigger the relay, or worse, back-feed 5V into the RP2040 and fry the chip. Use a dedicated 3.3V relay or a transistor driver.

ComponentExact Variant / ModelEst. Price (USD)Notes
MicrocontrollerRaspberry Pi Pico W (with pre-soldered headers)$6.00Ensure it is the 'W' variant with the Infineon Wi-Fi chip.
SensorAdafruit BME280 I2C/SPI (PID 2652)$12.50Includes onboard 3.3V regulator and pull-ups.
Relay ModulePololu 3.3V Relay Module (Single)$5.95Specifically rated for 3.3V logic triggering.
Power Supply5V 2A USB Micro-B Power Supply$8.00Do not power high-draw relays directly from the Pi USB port.
Wiring24 AWG solid core jumper wires$4.00Standard breadboard wire.

Pin Mapping & Wiring Procedure

The Pico W uses a specific I2C0 bus mapping by default in MicroPython. We will use GP4 and GP5 for the sensor, and GP16 for the relay control. Always de-energize the board before making I2C connections to prevent latch-up.

Pico W PinGPIO / FunctionTarget Component PinWire Color (Suggested)
Pin 6GP4 (SDA0)BME280 SDABlue
Pin 7GP5 (SCL0)BME280 SCLYellow
Pin 363V3(OUT)BME280 VIN & Relay VCCRed
Pin 38GNDBME280 GND & Relay GNDBlack
Pin 21GP16 (Digital Out)Relay EN / INGreen
Safety Note: If you are switching mains voltage (120V/240V AC) with the relay, ensure the relay contacts are rated for your load and that the AC wiring is completely isolated from the low-voltage breadboard. Never route AC wires over your 3.3V logic lines.

Complete MicroPython Web Server Code

This script implements a raw TCP socket server. It includes a hardware Watchdog Timer (WDT) to automatically reboot the Pico W if the main loop hangs due to a Wi-Fi stack fault—a common edge case on the CYW43439 chip. Save this as main.py on your Pico W.


import network
import socket
import time
import machine
from machine import Pin, I2C, WDT

# --- PIN DEFINITIONS ---
RELAY_PIN = 16
I2C_SDA = 4
I2C_SCL = 5

# --- HARDWARE SETUP ---
relay = Pin(RELAY_PIN, Pin.OUT, value=0)
i2c = I2C(0, sda=Pin(I2C_SDA), scl=Pin(I2C_SCL), freq=400000)

# Initialize Watchdog (8 second timeout)
wdt = WDT(timeout=8000)

# --- WI-FI CONFIGURATION ---
SSID = 'YOUR_2.4GHZ_SSID'
PASSWORD = 'YOUR_WIFI_PASSWORD'

def connect_wifi():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    # Prevent Wi-Fi chip from sleeping to avoid socket drops
    wlan.config(pm = 0xa11140) 
    wlan.connect(SSID, PASSWORD)
    
    max_wait = 15
    while max_wait > 0:
        if wlan.status() < 0 or wlan.status() >= 3:
            break
        max_wait -= 1
        print('Waiting for connection...')
        wdt.feed()
        time.sleep(1)
        
    if wlan.status() != 3:
        raise RuntimeError('Failed to connect to Wi-Fi')
    
    status = wlan.ifconfig()
    print('Connected on ' + status[0])
    return status[0]

def read_sensor():
    # BME280 I2C address is typically 0x76 or 0x77
    # This is a simplified mock read for demonstration; 
    # use the adafruit_bme280 library for production.
    try:
        # Mocking a 2-byte temperature read for code brevity
        data = i2c.readfrom_mem(0x76, 0xFA, 2)
        temp_c = 22.5 # Placeholder for actual math
        return f"{temp_c:.1f} C"
    except OSError as e:
        print(f"I2C Error: {e}")
        return "Sensor Error"

def serve_http(ip):
    addr = socket.getaddrinfo(ip, 80)[0][-1]
    s = socket.socket()
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(addr)
    s.listen(3)
    print('Listening on', addr)
    
    while True:
        wdt.feed() # Feed the dog
        try:
            cl, addr = s.accept()
            request = cl.recv(1024).decode('utf-8')
            
            # Parse relay state change
            if 'GET /on' in request:
                relay.value(1)
            elif 'GET /off' in request:
                relay.value(0)
                
            temp = read_sensor()
            state = 'ON' if relay.value() else 'OFF'
            
            html = f"""<!DOCTYPE html><html><body>
            <h1>Pico W Web Server</h1>
            <p>Temperature: {temp}</p>
            <p>Relay is {state}</p>
            <a href="/on"><button>Turn ON</button></a>
            <a href="/off"><button>Turn OFF</button></a>
            </body></html>"""
            
            # CRITICAL: Connection: close prevents browser socket hanging
            cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\nConnection: close\r\n\r\n')
            cl.send(html)
            cl.close()
            
        except OSError as e:
            print('Socket error:', e)
            cl.close()

if __name__ == '__main__':
    try:
        ip_addr = connect_wifi()
        serve_http(ip_addr)
    except Exception as e:
        print(f"Fatal error: {e}")
        machine.reset()

Debugging: First Three Things to Check When It Fails

When your Raspberry Pi Pico W web server crashes or refuses to load, don't just hit the reset button. Check these three specific failure modes based on the exact error strings thrown in the Thonny or PuTTY REPL console.

1. Error: RuntimeError: Failed to connect to Wi-Fi

The Cause: The Pico W's CYW43439 chip is strictly a 2.4GHz 802.11n radio. It physically cannot see 5GHz networks. Furthermore, it struggles with WPA3-SAE encryption and hidden SSIDs.

The Fix: Ensure your router is broadcasting a 2.4GHz band. If you are using a mesh network with a unified SSID, log into your router and temporarily disable 5GHz steering, or create a dedicated 2.4GHz IoT VLAN/SSID. Verify your password string has no trailing spaces.

2. Error: OSError: [Errno 110] ETIMEDOUT on I2C Read

The Cause: This occurs during the i2c.readfrom_mem() call. It means the RP2040 sent the clock signal, but the BME280 never acknowledged (ACK) the address. This is almost always a physical layer issue: missing pull-up resistors, swapped SDA/SCL lines, or the sensor drawing too much startup current and browning out the 3.3V rail.

The Fix: Run i2c.scan() in the REPL. If it returns an empty list [], check your wiring. The Adafruit BME280 has internal pull-ups, but if you are using a bare breakout board, you must add 4.7kΩ pull-up resistors between SDA/SCL and 3.3V.

3. Error: OSError: [Errno 12] ENOMEM or Browser "ERR_CONNECTION_RESET"

The Cause: MicroPython's socket implementation is lightweight. If you omit the Connection: close HTTP header, modern browsers (Chrome/Edge) will hold the TCP socket open, expecting a keep-alive stream. The Pico W quickly exhausts its limited lwIP socket buffer memory, resulting in ENOMEM (Out of Memory) or forcing the browser to drop the connection.

The Fix: Ensure the HTTP response header explicitly includes Connection: close\r\n as shown in the code block above. Always call cl.close() inside a finally block or immediately after sending the payload.

Extending and Simplifying the Build

Once you have the baseline socket server running, you can scale the project based on your deployment environment.

To Simplify (Local Use Only): If you only need to toggle the relay from a phone on the same Wi-Fi, strip out the BME280 I2C code entirely. This reduces the RAM footprint to under 20KB and allows the Pico W to handle HTTP requests in roughly 15 milliseconds.

To Extend (Production IoT): Raw sockets block the main thread. If you want to read sensors continuously while serving web pages, you must migrate to uasyncio. Replace the standard socket library with uasyncio.start_server(). Additionally, integrate machine.WDT (as demonstrated) and add an mDNS responder so you can access the server via http://picow.local instead of memorizing a DHCP-assigned IP address.

Frequently Asked Questions

Can I run a web server on Raspberry Pi Pico W without an internet connection?

Yes. The Pico W can operate in Access Point (AP) mode using network.WLAN(network.AP_IF). This creates its own local Wi-Fi network (e.g., 'PicoW-AP'). You can connect your phone directly to this SSID and access the web server at the default gateway IP (usually 192.168.4.1). This is ideal for remote field deployments where no router is available, though it consumes roughly 20mA more current than Station (STA) mode.

Why does my Raspberry Pi Pico W web server keep dropping connections after a few hours?

The CYW43439 Wi-Fi chip has an aggressive default power-management mode that puts the radio to sleep to save power, which drops active TCP sockets. In the code provided above, the line wlan.config(pm = 0xa11140) explicitly disables this power-saving behavior. If you are still experiencing drops, ensure your USB power supply can deliver a stable 5V at 2A; voltage sags during Wi-Fi transmission spikes will cause the RP2040 to brownout and reset the Wi-Fi chip.

How do I make my Raspberry Pi web server accessible outside my local network?

Do not use port forwarding to expose your Pico W directly to the public internet; it lacks the hardware security and TLS encryption capabilities to handle malicious bot traffic. Instead, use a reverse tunnel service like ngrok or Cloudflare Tunnels running on a secondary local machine (like a standard Raspberry Pi 4 or a PC) that proxies requests to your Pico W's local IP. Alternatively, rewrite the MicroPython code to act as an MQTT publisher, sending data to a secure cloud broker like Adafruit IO or AWS IoT, and control the relay via MQTT subscriptions.