Ultra-lightweight web monitoring for Raspberry Pi
Track uptime, response times, and get instant alerts โ all from a $35 computer
| Feature | Benefit |
|---|---|
| ๐ชถ Ultra-lightweight | Runs on Raspberry Pi 1B+ (512MB RAM) |
| ๐ Real-time Dashboard | CRT-style cyberpunk interface |
| ๐ JSON API | Integrate with anything |
| ๐พ Persistent Storage | SQLite keeps your history safe |
| โก Zero Config | Works out of the box |
How does WebStatusฯ compare to popular alternatives?
Docker Benchmark (5 URLs, 60s interval, 10 samples)
| Tool | RAM Usage | CPU Usage | Docker Image |
|---|---|---|---|
| WebStatusฯ | 26 MB | 0.2% | 66 MB |
| Statping-ng | 32 MB | 0.3% | 58 MB |
| Uptime Kuma | 124 MB | 0.5% | 439 MB |
Real-world on Raspberry Pi 1B+ (ARMv6, 512MB RAM)
| Metric | Value |
|---|---|
| RAM (application) | ~20 MB |
| RAM (with Python runtime) | ~34 MB |
| CPU | ~1% average |
| Storage | <1 MB (uses system Python) |
Installation Size (native, no Docker)
| Tool | Install Size | Requires |
|---|---|---|
| WebStatusฯ | <1 MB | System Python 3.7+ |
| Statping-ng | ~58 MB | Go binary |
| Uptime Kuma | ~150 MB | Node.js runtime |
Run ./benchmark/benchmark.sh to reproduce the Docker benchmark.
Run this on your Raspberry Pi:
curl -sSL https://raw.githubusercontent.com/jmlweb/webstatuspi/main/install.sh | bashThe interactive installer will:
- Install dependencies and create a virtual environment
- Guide you through URL configuration
- Optionally set up auto-start on boot
That's it! Open http://<your-pi-ip>:8080 in your browser.
๐ฆ Manual Installation
# Clone and install
git clone https://github.com/jmlweb/webstatuspi.git
cd webstatuspi
python3 -m venv venv
source venv/bin/activate
pip install .
# Configure
cp config.example.yaml config.yaml
# Edit config.yaml with your URLs
# Run
webstatuspiโ๏ธ Installer Options
# Interactive installation
./install.sh
# Non-interactive with defaults
./install.sh --non-interactive
# System-wide installation (with systemd service)
sudo ./install.sh --install-dir /opt/webstatuspi
# Update existing installation
./install.sh --update
# Uninstall
./install.sh --uninstall
Real-time status cards with latency and 24h uptime metrics. |
Click any card to see full check history with timestamps. |
Features:
- ๐ Auto-refresh every 10 seconds
- ๐ข๐ด Color-coded status indicators
- ๐ Response time graphs
- ๐น๏ธ Retro CRT aesthetic with scanlines
- ๐ฑ PWA Support - Install as app on mobile/desktop
The dashboard is a Progressive Web App that can be installed on your device for quick access:
Desktop (Chrome/Edge):
- Open the dashboard in your browser
- Click the install icon (โ) in the address bar
- Click "Install"
Mobile (Android):
- Open the dashboard in Chrome
- Tap the menu (โฎ) โ "Add to Home Screen"
- Tap "Install"
Mobile (iOS):
- Open the dashboard in Safari
- Tap the share button (โ)
- Tap "Add to Home Screen"
PWA Features:
- Works offline (shows cached data when network unavailable)
- App-like experience (no browser UI)
- Automatic updates when new versions are deployed
- Fast loading via Service Worker caching
Note: For production deployments, HTTPS is required for PWA features to work. On
localhost, PWA works without HTTPS for development.
| Method | Endpoint | Description |
|---|---|---|
GET |
/ |
Web dashboard |
GET |
/status |
All URLs status with 24h statistics |
GET |
/status/{name} |
Specific URL status |
GET |
/history/{name} |
Check history (last 24h, max 100 records) |
GET |
/health |
Health check |
GET |
/metrics |
Prometheus metrics |
GET |
/badge.svg |
Status badge (SVG image) |
DELETE |
/reset |
Reset all data (token auth if configured) |
curl http://localhost:8080/status{
"urls": [
{
"name": "MY_SITE",
"url": "https://example.com",
"is_up": true,
"status_code": 200,
"response_time_ms": 150,
"error": null,
"last_check": "2026-01-23T10:30:00Z",
"checks_24h": 1440,
"uptime_24h": 99.5,
"avg_response_time_24h": 145.2,
"min_response_time_24h": 120,
"max_response_time_24h": 200,
"p50_response_time_24h": 142,
"p95_response_time_24h": 185,
"p99_response_time_24h": 198,
"stddev_response_time_24h": 15.3,
"consecutive_failures": 0,
"ssl_cert_expires_in_days": 365
}
],
"summary": {
"total": 1,
"up": 1,
"down": 0
}
}Reset all monitoring data (useful for testing or fresh start):
# Without token (if api.reset_token not configured)
curl -X DELETE http://localhost:8080/reset
# With token authentication
curl -X DELETE http://localhost:8080/reset \
-H "Authorization: Bearer your-secret-token"Security notes:
- Blocked if accessed through Cloudflare (external access)
- Requires Bearer token if
api.reset_tokenis set in config
Embed a shields.io-style status badge in your README or website:
<!-- Overall system status -->

<!-- Specific service status -->
Badge colors:
| State | Color | Meaning |
|---|---|---|
| UP | Green | All services operational (or specific service is up) |
| DOWN | Red | All services down (or specific service is down) |
| DEGRADED | Yellow | Some services up, some down |
| UNKNOWN | Gray | No monitoring data available |
Headers:
Content-Type: image/svg+xmlCache-Control: public, max-age=60(CDN-friendly)
WebStatusฯ exposes metrics in Prometheus text format, allowing you to integrate with your existing monitoring stack and create custom Grafana dashboards.
GET /metrics
Returns metrics in Prometheus exposition format:
curl http://localhost:8080/metricsExample output:
# HELP webstatuspi_uptime_percentage Uptime percentage for the last 24 hours
# TYPE webstatuspi_uptime_percentage gauge
webstatuspi_uptime_percentage{url_name="MY_SITE",url="https://example.com"} 99.5
# HELP webstatuspi_response_time_ms Response time metrics in milliseconds
# TYPE webstatuspi_response_time_ms gauge
webstatuspi_response_time_ms{url_name="MY_SITE",url="https://example.com",type="avg"} 150.0
webstatuspi_response_time_ms{url_name="MY_SITE",url="https://example.com",type="min"} 120
webstatuspi_response_time_ms{url_name="MY_SITE",url="https://example.com",type="max"} 200
# HELP webstatuspi_checks_total Total number of checks performed
# TYPE webstatuspi_checks_total counter
webstatuspi_checks_total{url_name="MY_SITE",url="https://example.com",status="success"} 143
webstatuspi_checks_total{url_name="MY_SITE",url="https://example.com",status="failure"} 1
# HELP webstatuspi_last_check_timestamp Unix timestamp of last check
# TYPE webstatuspi_last_check_timestamp gauge
webstatuspi_last_check_timestamp{url_name="MY_SITE",url="https://example.com"} 1737544800
| Metric | Type | Description | Labels |
|---|---|---|---|
webstatuspi_uptime_percentage |
gauge | Uptime percentage for the last 24 hours (0-100) | url_name, url |
webstatuspi_response_time_ms |
gauge | Response time in milliseconds | url_name, url, type (avg/min/max) |
webstatuspi_checks_total |
counter | Total number of checks performed | url_name, url, status (success/failure) |
webstatuspi_last_check_timestamp |
gauge | Unix timestamp of last check | url_name, url |
Add this scrape config to your prometheus.yml:
scrape_configs:
- job_name: 'webstatuspi'
static_configs:
- targets: ['<raspberry-pi-ip>:8080']
metrics_path: /metrics
scrape_interval: 30sAverage uptime across all URLs:
avg(webstatuspi_uptime_percentage)
URLs with uptime below 99%:
webstatuspi_uptime_percentage < 99
Average response time per URL:
webstatuspi_response_time_ms{type="avg"}
Total failures in last hour:
increase(webstatuspi_checks_total{status="failure"}[1h])
URLs not checked in last 5 minutes:
(time() - webstatuspi_last_check_timestamp) > 300
Create custom Grafana dashboards using these metrics:
Example panels:
-
Uptime Overview (Gauge panel)
- Query:
avg(webstatuspi_uptime_percentage) - Thresholds: Red (<95%), Yellow (95-99%), Green (>99%)
- Query:
-
Response Time by URL (Graph panel)
- Query:
webstatuspi_response_time_ms{type="avg"} - Legend:
{{url_name}}
- Query:
-
Success Rate (Stat panel)
- Query:
sum(webstatuspi_checks_total{status="success"}) / sum(webstatuspi_checks_total) * 100
- Query:
-
Failed Checks (Last Hour) (Stat panel)
- Query:
increase(webstatuspi_checks_total{status="failure"}[1h])
- Query:
-
Check Status Table (Table panel)
- Queries:
webstatuspi_uptime_percentage(Uptime %)webstatuspi_response_time_ms{type="avg"}(Avg Response Time)time() - webstatuspi_last_check_timestamp(Last Check)
- Queries:
- Scraping frequency: Recommended 30-60 seconds to match typical check intervals
- Cardinality: Low - one metric series per URL (typically 5-20 URLs)
- Performance: Zero overhead - metrics are computed from existing database queries
- History: Metrics reflect 24-hour aggregates from SQLite database
Get instant notifications when URLs go down or recover. Integrate with Slack, Discord, PagerDuty, or any custom webhook endpoint.
Add to your config.yaml:
alerts:
webhooks:
- url: "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"
enabled: true
on_failure: true # Alert when URL goes DOWN
on_recovery: true # Alert when URL comes back UP
cooldown_seconds: 300 # Minimum time between alerts (prevents spam)| Service | Webhook Type | Notes |
|---|---|---|
| Slack | Incoming Webhook | Configure in Apps & integrations |
| Discord | Webhook URL | Copy webhook URL from channel settings |
| Telegram | Bot API | Setup guide - requires relay service |
| PagerDuty | Events API v2 | Sends to incidents/events endpoint |
| Custom HTTP | Any endpoint | Receives JSON payload |
Every alert sends this JSON structure:
{
"event": "url_down",
"url": {
"name": "API_SERVER",
"url": "https://api.example.com"
},
"status": {
"code": 503,
"success": false,
"response_time_ms": 5000,
"error": "Service Unavailable",
"timestamp": "2026-01-21T10:30:00Z"
},
"previous_status": "up"
}Monitor response times and get alerted when services become slow, even if they're still responding successfully. This helps detect performance degradation before it causes a complete outage.
Add latency thresholds to any URL in your configuration:
urls:
- name: "CRITICAL_API"
url: "https://api.example.com/health"
latency_threshold_ms: 2000 # Alert if response time exceeds 2000ms
latency_consecutive_checks: 3 # Must exceed threshold 3 times in a row (default: 3)
- name: "PAYMENT_API"
url: "https://payments.example.com"
latency_threshold_ms: 1000 # Alert if response time exceeds 1000ms
latency_consecutive_checks: 5 # Require 5 consecutive slow checks before alerting- Threshold Detection: After each check, if the response time exceeds
latency_threshold_ms, a counter increments. - Alert Trigger: When the counter reaches
latency_consecutive_checks, alatency_highalert is sent. - Recovery: When latency returns below the threshold, a
latency_normalalert is sent and the counter resets.
This prevents false positives from temporary network spikes while catching sustained performance issues.
Latency alerts use the same webhook infrastructure as up/down alerts, but with a different event type:
{
"event": "latency_high",
"url": {
"name": "CRITICAL_API",
"url": "https://api.example.com/health"
},
"latency": {
"current_ms": 2500,
"threshold_ms": 2000,
"consecutive_checks": 3
},
"timestamp": "2026-01-23T10:30:00Z"
}Event Types:
latency_high: Triggered when latency exceeds threshold for configured consecutive checkslatency_normal: Triggered when latency returns below threshold after an active alert
Note: Latency alerts respect the same cooldown_seconds setting as up/down alerts to prevent spam.
alerts:
webhooks:
- url: "https://example.com/webhook"
# Whether this webhook is active (default: true)
enabled: true
# Send alert when URL transitions from UP โ DOWN (default: true)
on_failure: true
# Send alert when URL transitions from DOWN โ UP (default: true)
on_recovery: true
# Minimum seconds between alerts for the same URL (default: 300)
# Prevents alert spam if a service is flaky
cooldown_seconds: 300Verify webhook configuration before deployment:
webstatuspi test-alertOutput:
Testing 2 webhook(s)...
โ SUCCESS: https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK
โ FAILED: https://example.com/broken-webhook
Result: 1/2 webhooks successful
For sensitive webhooks, use these patterns:
Option 1: Authentication Headers Configure your webhook endpoint to require an auth token, then test with curl:
curl -X POST https://example.com/webhook \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"event":"test"}'Option 2: HMAC Signature (Future) WebStatusฯ may support HMAC-SHA256 signatures in future versions.
- Create Slack Incoming Webhook: https://api.slack.com/apps โ Your App โ Incoming Webhooks
- Copy webhook URL
- Add to
config.yaml:
alerts:
webhooks:
- url: "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXX"
enabled: true
on_failure: true
on_recovery: true
cooldown_seconds: 300- Test:
webstatuspi test-alert - Your Slack channel will receive alerts when services go down
- Enable Developer Mode in Discord (User Settings โ Advanced)
- Right-click channel โ Edit Channel โ Webhooks โ Create Webhook
- Copy webhook URL
- Add to
config.yaml:
alerts:
webhooks:
- url: "https://discord.com/api/webhooks/123456789/abcdefg"
enabled: true
on_failure: true
on_recovery: false # Only alert on failures
cooldown_seconds: 600Install as a systemd service:
# Preview what will be installed
webstatuspi install-service --dry-run
# Install and start
sudo webstatuspi install-service --enable --start๐ Service management commands
sudo systemctl status webstatuspi # Check status
sudo journalctl -u webstatuspi -f # View live logs
sudo systemctl restart webstatuspi # Restart
sudo systemctl stop webstatuspi # Stopmonitor:
interval: 60 # seconds between checks
urls:
- name: "PROD_API" # max 10 characters
url: "https://api.example.com"
timeout: 10
- name: "STAGING"
url: "https://staging.example.com"
timeout: 5
server:
port: 8080
host: 0.0.0.0 # listen on all interfaces
database:
path: "./data/monitoring.db"
retention_days: 7 # auto-cleanup old dataMonitor not just HTTP status codes, but also response content. This helps detect when services return error pages with 200 status codes.
Keyword Validation - Check if response body contains a specific string:
urls:
- name: "API_PROD"
url: "https://api.example.com/health"
timeout: 10
keyword: "OK" # Case-sensitive substring checkJSON Path Validation - Check if JSON response has expected value at path:
urls:
- name: "API_STG"
url: "https://staging.example.com/api/status"
timeout: 10
json_path: "status.healthy" # Checks if response.status.healthy is truthyHow it works:
- Validation only runs if HTTP status is 2xx or 3xx
- If validation fails, URL is marked as DOWN with error message
- Response body limited to 1MB for memory efficiency (Pi 1B+)
- Keyword check: case-sensitive substring match
- JSON path: supports dot-notation (e.g.,
"data.user.active") - JSON values must be truthy (true, "ok", 1, etc.)
Use cases:
| Scenario | Solution |
|---|---|
| API returns error page with 200 status | Use keyword to check for success message |
| Health endpoint returns JSON status | Use json_path to check specific field |
| CDN returns cached error page | Use keyword to verify expected content |
| Backend returns maintenance page | Use keyword to detect unexpected content |
By default, HTTP status codes 200-399 are treated as success. You can customize this per URL using success_codes:
urls:
# Accept specific codes (e.g., API that returns 201 on create)
- name: "API_CREATE"
url: "https://api.example.com/create"
timeout: 10
success_codes: [200, 201, 202]
# Accept code ranges (e.g., legacy API where 400 means "no results")
- name: "API_LEGACY"
url: "https://legacy.example.com/api"
timeout: 10
success_codes: ["200-299", 400]Supported formats:
| Format | Example | Meaning |
|---|---|---|
| Single code | 200 |
Exactly status code 200 |
| Multiple codes | [200, 201, 202] |
Any of 200, 201, or 202 |
| Range | "200-299" |
200 to 299 inclusive |
| Mixed | ["200-299", 400] |
2xx range plus 400 |
Notes:
- If
success_codesis not specified, default range 200-399 is used - Status codes must be valid HTTP codes (100-599)
- Ranges use string format with hyphen:
"200-299"
Monitor non-HTTP services like databases, caches, and custom services by checking TCP port connectivity:
tcp:
# Database connectivity check
- name: "DB_PROD"
host: "db.example.com"
port: 5432
timeout: 5
# Redis cache check
- name: "REDIS"
host: "redis.example.com"
port: 6379
timeout: 3How it works:
| Condition | Result |
|---|---|
| TCP connection succeeds | Target marked UP |
| Connection refused | Target marked DOWN |
| Connection timeout | Target marked DOWN with timeout message |
Notes:
- TCP checks measure connection establishment time (response_time_ms)
- No HTTP protocol overhead - faster than HTTP checks
- Status code is
nullfor TCP checks (no HTTP status) - URL format in API:
tcp://host:port
Monitor DNS resolution for domain names to detect DNS propagation issues, server failures, or misconfigured records:
dns:
# Basic DNS resolution check
- name: "DNS_MAIN"
host: "example.com"
record_type: A # A (IPv4) or AAAA (IPv6)
timeout: 5
# Verify resolved IP matches expected value
- name: "DNS_API"
host: "api.example.com"
record_type: A
expected_ip: "192.0.2.1"How it works:
| Condition | Result |
|---|---|
| DNS resolution succeeds | Target marked UP |
| Resolution fails | Target marked DOWN with error |
| Resolved IP doesn't match expected | Target marked DOWN with mismatch message |
Supported record types:
| Type | Description |
|---|---|
A |
IPv4 address resolution (default) |
AAAA |
IPv6 address resolution |
Notes:
- DNS checks measure resolution time (response_time_ms)
- Uses OS DNS resolver (no additional dependencies)
- URL format in API:
dns://hostname
WebStatusฯ automatically monitors SSL certificates for all HTTPS URLs. No additional configuration is required.
What's monitored:
- Certificate expiration date
- Days until expiration (negative if expired)
- Certificate issuer (e.g., "Let's Encrypt")
- Certificate subject (common name)
Behavior:
| Condition | Result |
|---|---|
| Certificate valid | Normal monitoring, SSL info in API response |
| Certificate expires within threshold | Warning logged, URL still marked UP |
| Certificate expired | URL marked DOWN with error message |
| SSL extraction fails | URL status unaffected, error stored in ssl_cert_error |
Configuration:
monitor:
interval: 60
ssl_warning_days: 30 # Days before expiration to log warnings (default: 30)API Response (SSL fields):
{
"name": "MY_SITE",
"ssl_cert_issuer": "Let's Encrypt",
"ssl_cert_subject": "example.com",
"ssl_cert_expires_at": "2025-12-31T23:59:59Z",
"ssl_cert_expires_in_days": 365,
"ssl_cert_error": null
}Prometheus metric:
webstatuspi_ssl_cert_expires_in_days{url_name="MY_SITE",url="https://example.com",issuer="Let's Encrypt",subject="example.com"} 365
For Raspberry Pi 1B+:
| Setting | Recommendation |
|---|---|
| URLs | 5-10 max |
| Interval | 30+ seconds |
| Timeout | 10s or less |
Override any configuration value using environment variables with the WEBSTATUS_ prefix:
| Variable | Config Path | Example |
|---|---|---|
WEBSTATUS_API_PORT |
api.port |
8080 |
WEBSTATUS_API_RESET_TOKEN |
api.reset_token |
secret123 |
WEBSTATUS_MONITOR_INTERVAL |
monitor.interval |
60 |
WEBSTATUS_DATABASE_PATH |
database.path |
/data/status.db |
WEBSTATUS_DATABASE_RETENTION_DAYS |
database.retention_days |
7 |
Environment variables take precedence over config.yaml values.
WebStatusฯ includes several security measures:
| Feature | Description |
|---|---|
| Rate Limiting | 60 requests/min per IP (localhost exempt) |
| SSRF Protection | Blocks private IPs, localhost, dangerous ports |
| CSP Headers | Content Security Policy with nonces |
| Input Validation | URL names sanitized to prevent injection |
URLs configured for monitoring are validated to prevent Server-Side Request Forgery:
- Only
http://andhttps://schemes allowed - Private IP ranges blocked (10.x, 172.16.x, 192.168.x, localhost)
- Internal service ports blocked (22, 3306, 5432, 6379, etc.)
# Setup
python3 -m venv venv
source venv/bin/activate
pip install .[dev]
# Run tests
pytest tests/ -v
# Run benchmark (Docker required)
cd benchmark && ./benchmark.shwebstatuspi/
โโโ webstatuspi/ # Core package
โ โโโ __init__.py # CLI entry point
โ โโโ alerter.py # Webhook alerts
โ โโโ api.py # HTTP server
โ โโโ config.py # Configuration
โ โโโ database.py # SQLite operations
โ โโโ models.py # Data models
โ โโโ monitor.py # URL checker
โโโ tests/ # Test suite
โโโ docs/ # Documentation
- 0.96" OLED display support
- Physical button navigation
- Buzzer alerts on failures
- Status LEDs (green/red)
API not accessible from other devices
- Check firewall:
sudo ufw allow 8080 - Verify config has
host: 0.0.0.0 - Check Pi IP:
ip addr show
High CPU usage
- Increase polling interval to 60+ seconds
- Reduce number of monitored URLs
- Check for slow/timing out URLs
Connection timeouts
- Increase
timeoutvalue in config - Test network:
ping google.com - Test URL manually:
curl -I <url>
See Troubleshooting Guide for more.
| Document | Description |
|---|---|
| Architecture | System design & database schema |
| Hardware | GPIO pins & OLED setup |
| Telegram Setup | Push notifications via Telegram |
| Contributing | How to contribute |
| Development Rules | Code style & conventions |
MIT License โ see LICENSE for details.
Built with โค๏ธ for Raspberry Pi enthusiasts
Lightweight. Reliable. Open Source.

