Player experience on a game server is determined by three measurable metrics: ticks per second (TPS), player-to-server latency, and server uptime. When TPS drops below threshold, players experience rubber-banding, hit registration failures, and desync. When latency exceeds 100ms, competitive gameplay becomes frustrating. When the server crashes without auto-restart, players leave permanently. This guide covers monitoring all three with automated responses.
A 2025 analysis of 50,000 game servers found that servers maintaining 95%+ uptime and consistent TPS retain 2.8x more players over 30 days compared to servers with intermittent performance issues. The cost of monitoring (typically $5-20 per month) is negligible compared to the community impact of undetected server degradation.
---
TPS and MSPT Monitoring
TPS (ticks per second) measures how many game simulation cycles the server completes per second. MSPT (milliseconds per tick) measures how long each tick takes. These are inversely related: if MSPT exceeds the tick interval (33.3ms for 30 TPS, 50ms for 20 TPS), TPS drops below target.
| Game | Target TPS | Warning TPS | Critical TPS | Max MSPT |
| Minecraft (Java) | 20 | 18 | 15 | 50ms |
| Rust | 30 | 25 | 20 | 33ms |
| Valheim | 50 (fixed) | N/A | Server freeze | 20ms |
| ARK | 30 | 25 | 20 | 33ms |
| Terraria | 60 | 50 | 40 | 16ms |
Prometheus Exporter for Game Servers
<h1>game_server_exporter.py</h1>
from prometheus_client import start_http_server, Gauge, Counter
import time
import subprocess
import re
import json
from websocket import create_connection # pip install websocket-client
def rcon_command(host, port, password, command):
"""Send one command over Rust WebRCON and return the response text."""
ws = create_connection(f"ws://{host}:{port}/{password}", timeout=5)
try:
ws.send(json.dumps({"Identifier": 1, "Message": command, "Name": "exporter"}))
return json.loads(ws.recv()).get("Message", "")
finally:
ws.close()
<h1>Prometheus metrics</h1>
tps_gauge = Gauge("game_server_tps", "Current ticks per second", ["server_name"])
mspt_gauge = Gauge("game_server_mspt_ms", "Milliseconds per tick", ["server_name"])
players_gauge = Gauge("game_server_players", "Connected players", ["server_name"])
uptime_gauge = Gauge("game_server_uptime_seconds", "Server uptime", ["server_name"])
restarts_counter = Counter("game_server_restarts_total", "Total restarts", ["server_name"])
crashes_counter = Counter("game_server_crashes_total", "Total crashes", ["server_name"])
def collect_rcon_metrics(server_name, rcon_host, rcon_port, rcon_password):
"""Collect metrics via RCON for Rust-like servers."""
try:
# Query server performance via RCON
output = rcon_command(rcon_host, rcon_port, rcon_password, "perf")
# Parse TPS from output
tps_match = re.search(r"fps:\s+(\d+\.?\d*)", output)
if tps_match:
tps = float(tps_match.group(1))
tps_gauge.labels(server_name=server_name).set(tps)
if tps > 0:
mspt_gauge.labels(server_name=server_name).set(1000.0 / tps)
# Query player count
status = rcon_command(rcon_host, rcon_port, rcon_password, "status")
player_count = len(re.findall(r"\d+\.\d+\.\d+\.\d+", status))
players_gauge.labels(server_name=server_name).set(player_count)
except Exception as e:
print(f"Failed to collect metrics: {e}")
if __name__ == "__main__":
start_http_server(9101) # Prometheus scrape port
while True:
collect_rcon_metrics("rust-main", "localhost", 28016, "rcon_password")
time.sleep(15)
---
Player Latency Tracking
Player latency cannot be measured from the server alone. You need the round-trip time from each player client to the server.
<h1>Monitor network latency from server perspective</h1>
<h1>Track per-player ping via RCON status output</h1>
<h1>Rust: parse player pings from status command</h1>
<h1>Output: steamid, name, ping, connected, addr</h1>
<h1>76561198012345678, "PlayerName", 45, 3600, 10.0.0.1:28015</h1>
<h1>Prometheus recording rule for latency percentiles</h1>
<h1>prometheus.yml rules</h1>
groups:
<ul><li>name: game_server_latency</li>
</ul> rules:
- record: game_server_player_ping_p50
expr: histogram_quantile(0.50, game_server_player_ping_bucket)
- record: game_server_player_ping_p95
expr: histogram_quantile(0.95, game_server_player_ping_bucket)
- record: game_server_player_ping_p99
expr: histogram_quantile(0.99, game_server_player_ping_bucket)
| Latency Range | Player Experience | Suitable For |
| 0-30ms | Excellent, competitive viable | FPS, fighting games |
| 30-60ms | Good, slight delay noticeable | Most multiplayer games |
| 60-100ms | Acceptable, PvP disadvantage | Casual play, PvE |
| 100-150ms | Noticeable lag, hit reg issues | PvE only, building |
| 150ms+ | Unplayable for most games | Avoid |
---
Automated Crash Detection and Restart
#!/bin/bash
<h1>watchdog.sh - Game server crash detection and auto-restart</h1>
SERVER_NAME="rust-main"
PROCESS_NAME="RustDedicated"
START_SCRIPT="/home/rustserver/start_rust.sh"
LOG_FILE="/home/rustserver/watchdog.log"
DISCORD_WEBHOOK="https://discord.com/api/webhooks/YOUR_WEBHOOK"
HEALTH_CHECK_PORT=28016
MAX_RESTART_ATTEMPTS=3
RESTART_COUNT=0
COOLDOWN=300 # 5 minutes between restart attempts
log() {
echo "$(date +"%Y-%m-%d %H:%M:%S") [$SERVER_NAME] $1" >> "$LOG_FILE"
}
while true; do
# Check if process is running
if ! pgrep -f "$PROCESS_NAME" > /dev/null; then
log "CRASH DETECTED: Process not found"
if [ $RESTART_COUNT -ge $MAX_RESTART_ATTEMPTS ]; then
log "ERROR: Max restart attempts reached. Manual intervention needed."
# Send alert
curl -s -X POST "$DISCORD_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"content\": \"CRITICAL: $SERVER_NAME crashed and exceeded max restart attempts\"}"
sleep $COOLDOWN
RESTART_COUNT=0
continue
fi
RESTART_COUNT=$((RESTART_COUNT + 1))
log "Restarting server (attempt $RESTART_COUNT/$MAX_RESTART_ATTEMPTS)"
# Start server
cd /home/rustserver
nohup bash "$START_SCRIPT" > /dev/null 2>&1 &
# Wait for server to initialize
sleep 60
if pgrep -f "$PROCESS_NAME" > /dev/null; then
log "Server restarted successfully"
RESTART_COUNT=0
# Notify Discord
curl -s -X POST "$DISCORD_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"content\": \"$SERVER_NAME has been automatically restarted\"}"
else
log "Restart failed"
fi
else
RESTART_COUNT=0
fi
sleep 30
done
<h1>Run watchdog as systemd service</h1>
<h1>/etc/systemd/system/game-watchdog.service</h1>
[Unit]
Description=Game Server Watchdog
After=network.target
[Service]
Type=simple
User=rustserver
ExecStart=/home/rustserver/watchdog.sh
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
---
Grafana Dashboard
Build a game server dashboard that shows all critical metrics at a glance.
| Panel | Metric | Alert Threshold |
| TPS (current) | game_server_tps | Below 20 for 2 min |
| MSPT (current) | game_server_mspt_ms | Above 40ms for 2 min |
| Player count | game_server_players | N/A (informational) |
| Uptime | game_server_uptime_seconds | Reset = restart detected |
| Crash count (24h) | game_server_crashes_total | More than 3 per day |
| CPU usage | node_cpu_seconds_total | Above 90% for 5 min |
| RAM usage | node_memory_MemAvailable_bytes | Below 1 GB |
| Disk I/O | node_disk_io_time_seconds_total | Above 80% utilization |
| Network traffic | node_network_receive_bytes_total | Spike = possible DDoS |
| Player P95 ping | game_server_player_ping_p95 | Above 100ms |
<h1>Prometheus alerting rules for game servers</h1>
groups:
<ul><li>name: game_server_alerts</li>
</ul> rules:
- alert: GameServerTPSLow
expr: game_server_tps < 20
for: 2m
labels:
severity: high
annotations:
summary: "{{ $labels.server_name }} TPS is {{ $value }}"
- alert: GameServerCrashed
expr: up{job="game_server"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "{{ $labels.server_name }} is not responding"
- alert: GameServerHighPing
expr: game_server_player_ping_p95 > 100
for: 5m
labels:
severity: medium
annotations:
summary: "{{ $labels.server_name }} P95 player ping is {{ $value }}ms"
For Rust-specific server optimization, see our Rust game server hosting guide. For Valheim server setup and mod management, read our Valheim dedicated server hosting guide. For the infrastructure monitoring that underlies game server monitoring, check our VPS monitoring and alerting guide.
---
Frequently Asked Questions
What is the most important metric for game server health?
TPS (ticks per second) is the single most important metric. TPS directly measures the game simulation speed. When TPS drops, every player on the server experiences degradation simultaneously. CPU usage, RAM usage, and network metrics are supporting indicators that help diagnose TPS drops, but TPS is the primary measure of player experience.
How do I detect a DDoS attack on my game server?
Monitor inbound packet rate and bandwidth. A DDoS attack causes a sudden spike (10-100x normal) in UDP packets to your game port. Set up alerts for network traffic exceeding 3x the peak player-traffic baseline. Use iptables rate limiting as a first defense and upstream DDoS protection (OVH Game DDoS, Cloudflare Spectrum) for sustained attacks.
Should I restart the server on a schedule?
Yes, for most game servers. Scheduled restarts clear memory leaks, reset entity counts, and prevent performance degradation from extended uptime. Rust servers benefit from 6-12 hour restart cycles. Minecraft servers can run 24-48 hours between restarts. Valheim servers are stable for days but should restart weekly. Always warn players 5-10 minutes before a restart.
How do I monitor multiple game servers?
Use a centralized Prometheus and Grafana stack. Deploy the game server exporter on each server host, configure Prometheus to scrape all exporters, and create a Grafana dashboard with server selector dropdowns. This scales to hundreds of servers with minimal overhead. Each exporter uses under 10 MB RAM and negligible CPU.
What causes TPS drops that are not CPU-related?
Four common non-CPU causes: (1) Disk I/O during world saves (switch to NVMe). (2) Garbage collection pauses in Java-based servers like Minecraft (tune GC flags). (3) Network saturation from too many player connections or DDoS (check bandwidth metrics). (4) Database queries in modded servers with SQL-backed plugins (optimize queries or add caching).