Setting up a Raspberry Pi as HTTP server is the most direct way to bridge physical hardware with web-based dashboards or mobile apps. While you can use heavy frameworks, the most reliable approach for embedded IoT control is a lightweight Python Flask application running on a Raspberry Pi 5 (4GB) with Raspberry Pi OS (Bookworm, 64-bit). This combination leverages the Pi 5’s PCIe 2.0 lane and upgraded CPU to handle concurrent HTTP requests without dropping GPIO interrupts.
Difficulty: Intermediate (Requires basic Linux CLI and Python knowledge)
Time Estimate: 45 minutes
Target Board: Raspberry Pi 5 (4GB) running Bookworm OS
Hardware Spec Sheet & Parts List
Do not underpower your Pi 5. The board requires a strict 5V/5A (27W) USB-C PD profile to prevent brownouts when switching inductive loads like relays. If you use a standard phone charger, the Pi will throttle the USB ports and GPIO current limits.
| Component | Exact Model / Variant | Est. Price (2026) | Notes |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB RAM) | $60.00 | 8GB variant overkill for simple HTTP routing |
| Power Supply | Official 27W USB-C PD Supply | $12.00 | Must support 5V/5A PD profile |
| Storage | SanDisk Extreme 64GB microSD (A2) | $14.00 | A2 rating critical for database I/O |
| Thermal | Official Active Cooler | $5.00 | Required for sustained network loads |
| Actuator | Songle SRD-05VDC-SL-C Relay | $3.00 | Opto-isolated driver board preferred |
GPIO Pin Mapping & Wiring
Raspberry Pi OS Bookworm deprecated the legacy RPi.GPIO library in favor of gpiozero (which uses the lgpio backend under the hood). The code below targets BCM pin numbering. Ensure your physical wiring matches this mapping exactly to avoid shorting the 3.3V rail.
| Physical Pin | BCM GPIO | Component | Wire Color |
|---|---|---|---|
| Pin 11 | GPIO 17 | Relay IN (Signal) | Orange |
| Pin 13 | GPIO 27 | Pushbutton (Signal) | Yellow |
| Pin 6 | GND | Relay GND / Button GND | Black |
| Pin 2 | 5V Power | Relay VCC | Red |
The Python HTTP Server Code
This script uses Flask to create RESTful endpoints. It includes proper hardware cleanup on exit, which is critical in Bookworm to prevent the lgpio daemon from locking the pin states after a crash.
from flask import Flask, jsonify, request
from gpiozero import LED, Button
import logging
import sys
# --- PIN DEFINITIONS (BCM Numbering) ---
RELAY_PIN = 17 # Physical Pin 11
BUTTON_PIN = 27 # Physical Pin 13
# --- HARDWARE INITIALIZATION ---
# active_high=False assumes a low-level trigger relay module
relay = LED(RELAY_PIN, active_high=False)
button = Button(BUTTON_PIN, pull_up=True, bounce_time=0.05)
app = Flask(__name__)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
@app.route('/api/status', methods=['GET'])
def get_status():
"""Returns current state of relay and physical button."""
return jsonify({
"relay_active": relay.is_lit,
"button_pressed": button.is_pressed
}), 200
@app.route('/api/relay', methods=['POST'])
def set_relay():
"""Toggles relay based on JSON payload: {"state": "on"|"off"}"""
data = request.get_json(silent=True)
if not data or 'state' not in data:
return jsonify({"error": "Missing JSON payload. Use {'state': 'on'}"}), 400
state = data['state'].lower()
if state == 'on':
relay.on()
logging.info("Relay ENGAGED via HTTP")
return jsonify({"message": "Relay activated"}), 200
elif state == 'off':
relay.off()
logging.info("Relay DISENGAGED via HTTP")
return jsonify({"message": "Relay deactivated"}), 200
return jsonify({"error": "Invalid state. Use 'on' or 'off'"}), 400
if __name__ == '__main__':
try:
logging.info("Starting HTTP Server on port 5000...")
# Bind to 0.0.0.0 to allow LAN access; debug=False for stability
app.run(host='0.0.0.0', port=5000, debug=False)
except OSError as e:
logging.error(f"Failed to bind port: {e}")
sys.exit(1)
except KeyboardInterrupt:
logging.info("Keyboard interrupt received. Shutting down safely.")
finally:
# Crucial for Bookworm OS to release lgpio locks
relay.close()
button.close()
logging.info("GPIO pins released.")
Debugging: First Three Things to Check When It Fails
When your Raspberry Pi as HTTP server refuses connections or crashes on boot, do not rewrite the code. Check these three specific failure modes first.
1. The Port Binding Conflict
Exact Error String: OSError: [Errno 98] Address already in use
Ranked Causes:
- Zombie Python Process: You killed the script previously with
kill -9before the socket timed out. Fix: Runsudo fuser -k 5000/tcpto clear the port. - Apache2/Nginx Interference: If you installed a LAMP stack previously, a web server might be hogging port 80/8080 (if you changed the Flask port). Fix:
sudo systemctl stop apache2.
2. The GPIO Lock Error
Exact Error String: gpiozero.exc.GPIOPinInUse: pin 17 is already in use
Ranked Causes:
- Unclean Exit: A previous script crashed without hitting the
finallyblock, leaving thelgpiochip select high. Fix: Reboot the Pi (sudo reboot) or rungpiozeropin factory reset in a Python REPL. - I2C/SPI Conflict: You enabled an overlay in
/boot/firmware/config.txtthat claims GPIO 17. Fix: Checkraspi-configinterface settings.
3. The Silent LAN Rejection
Exact Error String: Connection refused (when pinging from your desktop via curl)
Ranked Causes:
- Binding to Localhost: Your code says
host='127.0.0.1'instead ofhost='0.0.0.0'. The server is only listening to the Pi itself. Fix: Update theapp.run()parameter. - UFW Firewall: You enabled Uncomplicated Firewall but forgot to open port 5000. Fix:
sudo ufw allow 5000/tcp.
Extending and Simplifying the Build
The Flask development server (app.run()) is single-threaded and will drop requests if two users hit the API simultaneously. To make this production-ready for a smart home environment, you must extend the stack.
To Extend (Production Ready):
Install Gunicorn (pip install gunicorn) to handle concurrent worker threads, and place Nginx in front of it as a reverse proxy to handle SSL termination and static file caching. You can run Gunicorn as a systemd service so it automatically restarts on power loss. See the Flask Gunicorn deployment guide for the exact systemd unit file syntax.
To Simplify (Read-Only Sensors):
If you only need to serve a single static JSON payload (e.g., a temperature reading) and don't need POST routing, drop Flask entirely. Use Python’s built-in http.server module combined with a cron job that writes sensor data to a data.json file in the /var/www/html directory. This reduces RAM overhead by roughly 40MB, which is vital if you downgrade to a Pi Zero 2 W.
Frequently Asked Questions
Can I use a Raspberry Pi Zero 2 W as an HTTP server for battery-powered IoT?
Yes, but with strict caveats. The Zero 2 W has 512MB of RAM and lacks the Pi 5’s thermal mass. Running Flask + Gunicorn will consume about 60-80MB of RAM, leaving enough for the OS, but you must disable the HDMI output and Bluetooth (dtoverlay=disable-bt in config.txt) to drop idle current draw below 120mA. For battery setups, consider switching from Flask to MicroPython on an ESP32 instead, as the Pi Zero will drain a standard 18650 cell in under 14 hours even when sleeping.
How do I expose my Raspberry Pi HTTP server to the public internet securely?
Never use port forwarding on your home router to expose port 5000 directly; Flask is not designed to handle malicious edge-case HTTP headers and DDoS traffic. The correct architecture is to use a reverse proxy like Nginx with Let's Encrypt SSL, or use a tunneling service like Cloudflare Tunnels (cloudflared). Cloudflare Tunnels are preferred for embedded devs because they bypass CGNAT (Carrier-Grade NAT) issues common with cellular backup connections and require zero router configuration.
Is Python Flask fast enough for high-frequency sensor logging on a Pi 5?
For standard smart home polling (1-5 requests per second), Flask on a Pi 5 is more than adequate, returning JSON payloads in under 4ms. However, if you are logging high-frequency vibration or audio data (100+ Hz), Python's Global Interpreter Lock (GIL) and Flask's WSGI overhead will cause buffer overruns and dropped packets. For high-frequency ingestion, bypass HTTP entirely and use MQTT (Mosquitto broker) or write directly to a local InfluxDB instance using the Python UDP client.






