DEV Community

Hive80-lab
Hive80-lab

Posted on

The $14 Alternative to $500/Month Monitoring Stacks

The $14 Alternative to $500/Month Monitoring Stacks

You don't need Datadog, New Relic, AND PagerDuty. You need a script and a cron job.

I audited a startup's monitoring stack last month. They were paying:

  • Datadog: $300/month (5 hosts, custom metrics)
  • PagerDuty: $99/month (5 users)
  • StatusPage: $99/month
  • Total: $498/month

They had 12 employees. Their infrastructure was 5 servers and a managed database. They were paying more for monitoring than for their actual infrastructure.

Here's what I set up instead for $14/month.

The $14 Stack

Component Tool Cost
Monitoring Custom script + UptimeRobot free $0
Alerting Slack webhooks + custom script $0
Status page GitHub Pages + custom script $0
Log aggregation Loki (self-hosted) $0
VPS for scripts Cheapest DigitalOcean droplet $14
Total $14/month

Annual savings: $5,808

Component 1: Monitoring (Replaces Datadog)

#!/usr/bin/env python3
"""monitor.py - Lightweight monitoring that covers 90% of needs"""
import requests
import time
import json
from datetime import datetime
import subprocess

class Monitor:
    def __init__(self, config_file='monitor_config.json'):
        with open(config_file) as f:
            self.config = json.load(f)
        self.history = []

    def check_http(self, url, expected_code=200):
        start = time.time()
        try:
            r = requests.get(url, timeout=10)
            return {
                'type': 'http',
                'target': url,
                'status': r.status_code,
                'healthy': r.status_code == expected_code,
                'latency_ms': round((time.time() - start) * 1000, 2),
                'timestamp': datetime.now().isoformat()
            }
        except Exception as e:
            return {
                'type': 'http', 'target': url,
                'status': 0, 'healthy': False,
                'error': str(e),
                'timestamp': datetime.now().isoformat()
            }

    def check_disk(self, threshold=80):
        result = subprocess.run(['df', '-h', '/'], capture_output=True, text=True)
        usage = int(result.stdout.split('\n')[1].split()[4].replace('%', ''))
        return {
            'type': 'disk', 'target': '/',
            'usage_pct': usage,
            'healthy': usage < threshold,
            'timestamp': datetime.now().isoformat()
        }

    def check_memory(self, threshold=90):
        with open('/proc/meminfo') as f:
            lines = f.readlines()
        total = int(lines[0].split()[1])
        available = int(lines[2].split()[1])
        usage = ((total - available) / total) * 100
        return {
            'type': 'memory', 'usage_pct': round(usage, 1),
            'healthy': usage < threshold,
            'timestamp': datetime.now().isoformat()
        }

    def check_cpu(self, threshold=90):
        result = subprocess.run(['top', '-bn1'], capture_output=True, text=True)
        cpu_line = [l for l in result.stdout.split('\n') if 'Cpu(s)' in l][0]
        idle = float(cpu_line.split(',')[3].strip().replace(' id', ''))
        usage = 100 - idle
        return {
            'type': 'cpu', 'usage_pct': round(usage, 1),
            'healthy': usage < threshold,
            'timestamp': datetime.now().isoformat()
        }

    def run_all(self):
        results = []
        for check in self.config.get('checks', []):
            if check['type'] == 'http':
                results.append(self.check_http(check['url']))
            elif check['type'] == 'disk':
                results.append(self.check_disk(check.get('threshold', 80)))
            elif check['type'] == 'memory':
                results.append(self.check_memory(check.get('threshold', 90)))
            elif check['type'] == 'cpu':
                results.append(self.check_cpu(check.get('threshold', 90)))
        return results

if __name__ == '__main__':
    monitor = Monitor()
    results = monitor.run_all()
    unhealthy = [r for r in results if not r.get('healthy')]
    if unhealthy:
        alert(f'{len(unhealthy)} checks failed!')
    print(json.dumps(results, indent=2))
Enter fullscreen mode Exit fullscreen mode

Component 2: Alerting (Replaces PagerDuty)

#!/usr/bin/env python3
"""alert.py - Multi-channel alerting"""
import requests
import os
from datetime import datetime

class Alerter:
    def __init__(self):
        self.slack_webhook = os.environ.get('SLACK_WEBHOOK')
        self.sms_key = os.environ.get('TWILIO_KEY')  # Optional

    def slack(self, message, channel='#alerts'):
        if not self.slack_webhook:
            return
        payload = {
            'channel': channel,
            'text': message,
            'attachments': [{
                'color': 'danger' if 'FAILED' in message else 'good',
                'fields': [
                    {'title': 'Timestamp', 'value': datetime.now().isoformat(), 'short': True}
                ]
            }]
        }
        requests.post(self.slack_webhook, json=payload)

    def escalate(self, message, primary, secondary, delay_min=5):
        """Page primary, escalate to secondary after delay"""
        self.slack(f'🚨 {message}')
        # Wait and escalate if not acknowledged
        time.sleep(delay_min * 60)
        if not check_acknowledged():
            self.slack(f'⏰ ESCALATION: {message}')
            self.sms(primary, message)
Enter fullscreen mode Exit fullscreen mode

Component 3: Status Page (Replaces StatusPage)

<!-- status.html - Host on GitHub Pages for free -->
<!DOCTYPE html>
<html>
<head>
    <title>System Status</title>
    <meta http-equiv="refresh" content="60">
</head>
<body>
    <h1>System Status</h1>
    <div id="status"></div>
    <script>
    fetch('/status.json')
        .then(r => r.json())
        .then(data => {
            const html = data.map(check => `
                <div style="padding: 10px; border: 1px solid #ddd; margin: 5px;">
                    <span style="color: ${check.healthy ? 'green' : 'red'}">
                        ${check.healthy ? '' : ''}
                    </span>
                    <strong>${check.target || check.type}</strong>
                    ${check.latency_ms ? `(${check.latency_ms}ms)` : ''}
                </div>
            `).join('');
            document.getElementById('status').innerHTML = html;
        });
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The Cron Setup

# /etc/crontab
* * * * * root /opt/monitor/monitor.py >> /var/log/monitor.log
*/5 * * * * root /opt/monitor/check_alerts.py
0 * * * * root /opt/monitor/update_status_page.py
Enter fullscreen mode Exit fullscreen mode

What You Get vs What You Lose

What you GET:

  • Full control over your monitoring data
  • No vendor lock-in
  • Customizable alerts and thresholds
  • All data stays on your infrastructure
  • $5,808/year in savings

What you LOSE:

  • Beautiful pre-built dashboards (you build your own with Grafana — free)
  • AI-powered anomaly detection (you set static thresholds)
  • One-click integrations (you write 10 lines of code per integration)
  • 24/7 support (you're on your own)

When to Keep the $500 Stack

Keep paying if:

  • You have 50+ servers (the script doesn't scale well past that)
  • You need SOC2/HIPAA compliance reporting (Datadog handles this)
  • Your team is non-technical (the UI is worth the cost)
  • You have budget and no time (the $500 buys convenience)

When to Switch

Switch if:

  • You have fewer than 20 servers
  • You have a technical team member who can maintain scripts
  • You're pre-revenue or bootstrapped
  • You want to understand your infrastructure (not just monitor it)

Want the complete $14 monitoring stack? The Ops Starter Kit includes all the monitoring scripts, alert templates, status page code, and setup instructions — everything you need to replace your $500/month stack.

How much are you paying for monitoring? Could $14/month work for you?

Top comments (0)