Turning a Raspberry Pi into a networked I/O controller is one of the most practical embedded projects you can build. By hosting a lightweight API, you can trigger physical relays from a smartphone dashboard, a Home Assistant webhook, or a simple cURL command. But while the concept is simple, the hardware realities of the Raspberry Pi 5—specifically its 3.3V logic levels and strict USB-C PD power negotiation—trip up many builders.
This guide gives you the exact decision path, hardware specs, pin mapping, and production-ready Python code to build a reliable Raspberry Pi HTTP server for GPIO relay control.
The Decision Path: Which Board and Framework?
Before buying parts, you need to lock in your architecture. The Raspberry Pi ecosystem offers multiple boards and Python web frameworks, but they are not interchangeable for hardware control.
| Requirement | Option A | Option B | The Concrete Pick |
|---|---|---|---|
| Web Framework | Flask (Synchronous, lightweight, massive community) | FastAPI (Asynchronous, auto-docs, higher overhead) | Flask. For <100 req/sec GPIO toggling, Flask's simplicity and lower memory footprint win. |
| Board Variant | Raspberry Pi 4 Model B (4GB) | Raspberry Pi 5 (4GB) | Raspberry Pi 5 (4GB). The new RP1 southbridge handles I/O more efficiently, and it is the current production standard. |
| GPIO Library | RPi.GPIO (Legacy, C-extension) | gpiozero (Modern, Pythonic, lgpio backend) | gpiozero. RPi.GPIO is officially deprecated for Pi 5; gpiozero with the lgpio pin factory is the supported standard. |
| Relay Module | Standard 5V Relay (requires 5V logic trigger) | 3.3V Logic Relay Module (optocoupler isolated) | 3.3V Logic Relay. Feeding 5V back into a Pi 5 GPIO pin will fry the RP1 chip. |
Hardware Spec Sheet & Parts List
Do not substitute the power supply. The Raspberry Pi 5 requires a 27W USB-C Power Delivery (PD) supply to unlock the full 1.6A current limit on the 5V GPIO rail. If you use a standard 15W phone charger, the Pi restricts the 5V pins to 0.6A, which will cause mechanical relays to chatter or fail to pull in under load.
| Component | Exact Variant / Model | Estimated Cost | Why This Specific Part |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB RAM) | $60.00 | 4GB is plenty for headless Flask; 8GB is wasted money for this use case. |
| Power Supply | Official Raspberry Pi 27W USB-C PD (5V/5A) | $12.00 | Ensures 1.6A 5V rail current. Prevents GPIO brownouts. |
| Thermal Mgmt | Official Active Cooler or Aluminum Heatsink Case | $10.00 | Pi 5 throttles at 85°C. A passive block is insufficient for sustained network loads. |
| Relay Module | Sunfounder or Osoyee 4-Channel 3.3V Relay | $9.00 | Must explicitly state 3.3V trigger. Standard 5V modules will damage the Pi. |
| Storage | 32GB SanDisk Extreme microSD (A2 rated) | $12.00 | A2 rating handles the random I/O writes of Flask logging without corrupting the OS. |
| Wiring | Female-to-Female Jumper Wires (20cm, 22 AWG) | $5.00 | Standard DuPont connectors for Pi header to relay screw terminals. |
Pin Mapping: Wiring the Pi 5 to a 3.3V Relay
The Raspberry Pi 5 uses the BCM (Broadcom) pin numbering scheme in software, but physical board pin numbers for wiring. Most relay modules are active-LOW, meaning they trigger when the GPIO pin is pulled to Ground (0V), not when it is driven High (3.3V).
| Pi 5 Physical Pin | BCM GPIO Number | Relay Module Pin | Function / Notes |
|---|---|---|---|
| Pin 2 | N/A (5V Power) | VCC (or JD-VCC) | Powers the relay coils. See warning below regarding the jumper cap. |
| Pin 6 | N/A (Ground) | GND | Common ground reference. |
| Pin 11 | GPIO 17 | IN1 | Control signal for Relay 1 (Active-LOW). |
| Pin 13 | GPIO 27 | IN2 | Control signal for Relay 2 (Active-LOW). |
Many 4-channel relay modules have a blue jumper cap bridging
VCC and JD-VCC. If you are using a module with an optocoupler isolation design, remove this jumper. Connect Pi Pin 2 (5V) to JD-VCC, and connect the Relay module's VCC to a separate 5V source (or leave it tied to the Pi's 3.3V pin if the module supports it, though 5V is safer for coil pull-in). If you leave the jumper on and wire 5V into the Pi's 3.3V logic pins via a miswired ground loop, you will permanently destroy the RP1 I/O bank.
The Code: Flask HTTP Server with Error Handling
This implementation targets the Raspberry Pi 5 (4GB) running Raspberry Pi OS Lite (Bookworm or newer). It uses gpiozero with the lgpio backend, which is the officially supported method for Pi 5 GPIO access in 2026. We configure the OutputDevice with active_high=False to accommodate the active-LOW trigger of standard relay modules.
First, install the dependencies via your terminal:
sudo apt update
sudo apt install python3-flask python3-gpiozero python3-rpi-lgpio
Save the following code as server.py:
from flask import Flask, jsonify
from gpiozero import OutputDevice
import logging
import sys
# Configure logging to catch HTTP and GPIO events
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
# Pin Definitions (BCM numbering)
# active_high=False because most relay modules trigger on GND (Active-LOW)
RELAY_PINS = {
1: OutputDevice(17, active_high=False, initial_value=False),
2: OutputDevice(27, active_high=False, initial_value=False)
}
@app.route('/api/relay/<int:relay_id>/<string:state>', methods=['GET'])
def control_relay(relay_id, state):
"""Endpoint to toggle relays. Example: /api/relay/1/on"""
if relay_id not in RELAY_PINS:
logger.warning(f'Invalid relay ID requested: {relay_id}')
return jsonify({'error': 'Invalid relay ID. Use 1 or 2.'}), 404
relay = RELAY_PINS[relay_id]
try:
if state.lower() == 'on':
relay.on()
logger.info(f'Relay {relay_id} engaged.')
elif state.lower() == 'off':
relay.off()
logger.info(f'Relay {relay_id} disengaged.')
else:
return jsonify({'error': 'State must be exactly "on" or "off".'}), 400
return jsonify({
'relay': relay_id,
'state': state.lower(),
'status': 'success'
}), 200
except Exception as e:
logger.error(f'Hardware GPIO Error on Relay {relay_id}: {e}')
return jsonify({'error': 'Hardware failure', 'details': str(e)}), 500
@app.route('/api/status', methods=['GET'])
def get_status():
"""Returns the current state of all mapped relays."""
states = {rid: 'on' if dev.value else 'off' for rid, dev in RELAY_PINS.items()}
return jsonify({'relays': states}), 200
if __name__ == '__main__':
logger.info('Starting Raspberry Pi HTTP Server on port 5000...')
try:
# host='0.0.0.0' makes it accessible on the local network
app.run(host='0.0.0.0', port=5000, debug=False)
except KeyboardInterrupt:
logger.info('Server interrupted by user. Cleaning up GPIO...')
except Exception as fatal:
logger.critical(f'Server crashed: {fatal}')
sys.exit(1)
finally:
# gpiozero handles cleanup automatically on exit, but we force it here
for relay in RELAY_PINS.values():
relay.close()
logger.info('GPIO pins released safely.')
Run the server using python3 server.py. You can test it from another machine on your network by navigating to http://<PI_IP_ADDRESS>:5000/api/relay/1/on.
Debugging: Exact Error Strings and the First Three Checks
When embedding hardware with network stacks, failures usually happen at the intersection of OS permissions, port binding, and silicon logic. If your server fails to start or the relays don't click, follow this decision path.
The First Three Things to Check When It Fails
- Power Supply Negotiation: Run
vcgencmd get_throttledin the terminal. If it returns anything other thanthrottled=0x0, your Pi is brownout-throttling. The 5V rail is sagging, and the relay coils aren't getting enough current to pull the electromagnet. Swap to the official 27W PD supply. - Pin Factory Backend: The Pi 5 does not use the legacy
/dev/memGPIO interface. Ifgpiozerofails to initialize, you are almost certainly missing thelgpioC-extension binding. - Multimeter Verification: Disconnect the relay IN wires. Run the code and toggle the API. Use a multimeter to measure the voltage between the Pi's GND pin and GPIO 17. It should read ~3.3V when OFF, and drop to ~0.0V when ON. If it stays at 3.3V, your software state is inverted or the pin is dead.
Exact Error Strings and Ranked Causes
gpiozero.exc.BadPinFactory: Unable to load any default pin factory!Rank 1 Cause: Missing
rpi-lgpio package on Raspberry Pi 5 (Bookworm OS).Fix: Run
sudo apt install python3-rpi-lgpio. Do not try to install RPi.GPIO via pip; it will fail to compile on Pi 5.Error String:
OSError: [Errno 98] Address already in useRank 1 Cause: A previous instance of the Flask app crashed or was killed without releasing port 5000.
Fix: Run
sudo fuser -k 5000/tcp to force-kill the zombie process holding the socket, then restart your script.Error String:
PermissionError: [Errno 13] Permission denied (during GPIO init)Rank 1 Cause: The current user is not in the
gpio group, or lgpio is being blocked by systemd restrictions.Fix: Run
sudo usermod -aG gpio $USER, log out, and log back in. Avoid running the Flask app as root (sudo) as it exposes your network stack to unnecessary privilege escalation risks.
Extending and Simplifying the Build
Once the baseline HTTP server is stable, you have two distinct paths depending on your end goal.
How to Simplify (For Local/Home Automation)
If you only need to trigger these relays from Home Assistant or a local dashboard, drop Flask entirely and use MQTT. By using the paho-mqtt library, the Pi becomes a subscriber client. This eliminates the need to manage HTTP ports, reverse proxies, and REST API state parsing. Home Assistant has native MQTT auto-discovery, meaning your relays will appear as native switches in the UI with zero custom YAML coding.
How to Extend (For Production/Remote Access)
If you intend to expose this Raspberry Pi HTTP server to the wider internet or integrate it into a larger commercial IoT fleet, you must secure the transport layer.
1. Never expose port 5000 directly to the internet. Flask's built-in Werkzeug server is not designed for production security.
2. Install Nginx (sudo apt install nginx) and configure it as a reverse proxy to forward traffic from port 80/443 to localhost:5000.
3. Use Certbot to generate a free Let's Encrypt SSL certificate, ensuring your API commands are encrypted in transit.
4. Finally, wrap your Python script in a systemd service so it automatically restarts if the Pi loses power or the script encounters an unhandled memory fault.
For comprehensive API design patterns, refer to the official Flask documentation, and for hardware-safe GPIO abstraction, consult the gpiozero API reference. By respecting the Pi 5's power delivery requirements and using the correct 3.3V logic isolation, your embedded HTTP server will run reliably for years without hardware degradation.






