Skip to the content.

Pi-hole Latency Stats is a lightweight, zero-dependency Bash script that analyzes your Pi-hole’s performance. It calculates latency percentiles (Median, 95th), groups query speeds into “Tiers” (buckets), and—optionally—monitors your Unbound recursive DNS server statistics and memory usage.

This tool helps you answer: “Is my DNS slow because of my upstream provider, or is it just my local network?” and “Is Unbound performing efficiently?”

Features

Requirements

Usage

You can run the script with various flags to customize the analysis.

sudo ./pihole_stats.sh [OPTIONS]

🕒 Time Filters

🔍 Query Modes

🎯 Filtering

📦 Unbound Integration

🖥️ Display Options

💾 Output & Automation

⚙️ Configuration

Configuration File

On the first run, the script creates pihole_stats.conf in the same directory. You can edit this file to permanently set your preferences:

  1. Define Latency Tiers: Customize your buckets (e.g., L01="0.5" for 0.5ms).
  2. Default Save Directory: Set SAVE_DIR to a folder where -f files will be saved automatically.
  3. Unbound Settings: Set ENABLE_UNBOUND to auto (default), true (always on), or false.
  4. Visual Layout: Set LAYOUT to auto (default), horizontal, or vertical.
  5. Auto-Delete: Set MAX_LOG_AGE to automatically delete old reports every time the script runs.

🔍 Real-World Use Cases

1. 🐢 Diagnosing “Is it me or the ISP?”

When your internet feels slow, speed tests often lie because they measure bandwidth, not latency. DNS lag is the #1 cause of “snappy” browsing turning sluggish.

2. 🚀 Optimizing Unbound Performance

If you use Unbound (recursive or forwarding), blind trust isn’t enough. Verify your cache efficiency.

3. 🕵️ Domain-Specific Debugging

Sometimes specific services (like work VPNs or streaming sites) feel slow while everything else is fine.

4. 📉 Long-Term Health Monitoring

Spot trends before they become problems by automating data collection.

5. 🛡️ Safe Analysis on Low-End Hardware

Running heavy SQL queries on a Raspberry Pi Zero (512MB RAM) can cause the web interface to freeze or FTL to crash (“Database Locked”).

Unbound Integration

The script attempts to Auto-Detect Unbound. It checks if:

  1. Unbound is installed and the service is active.
  2. Pi-hole is configured to use it.

⚠️ Prerequisite for Memory Stats

To see the Memory Usage breakdown (Message vs RRset cache), you must enable extended statistics in Unbound.

  1. Edit your config: sudo nano /etc/unbound/unbound.conf (or your specific config file).
  2. Add extended-statistics: yes inside the server: block:
server:
    # ... other settings ...
    extended-statistics: yes

  1. Restart Unbound: sudo service unbound restart

Without this setting, Memory Usage will report 0 MB.

⚠️ Performance Note: Unbound Cache Counting (-ucc)

The -ucc flag provides deep insights by counting the exact number of items in your Unbound RAM cache. To achieve this, it triggers a cache dump.

Please use this flag responsibly:

Understanding the Metrics

Pi-hole Metrics

Unbound Metrics

Automated Reports (Cron)

To generate a daily report at 11:55 PM and auto-delete logs older than 30 days:

# Open crontab
crontab -e

# Add this line:
55 23 * * * /home/pi/pihole_stats.sh -24h -j -f "daily_stats.json" -rt 30 -s

ℹ️ Layout Note: When running via Cron, the script cannot detect a screen width and will automatically default to the Vertical layout for text reports. This ensures logs are readable and don’t wrap incorrectly.

Example Output

Click to expand Text Report (Horizontal Layout)
*Automatically generated on terminals wider than 100 columns (or with `-hor`).* Horizontal Statistics
Click to expand Text Report (Vertical Layout)
*Standard layout for mobile, cron logs, or `-ver` flag.* Vertical Statistics
Click to expand JSON Structure
{
  "version": "3.1",
  "date": "2026-01-20 12:30:00",
  "time_period": "All Time",
  "mode": "All Normal Queries",
  "stats": {
    "total_queries": 165711,
    "unsuccessful": 2840,
    "total_valid": 162871,
    "blocked": 23329,
    "analyzed": 139542
  },
  "latency": {
    "average": 6.55,
    "median": 0.03,
    "p95": 14.96
  },
  "tiers": [
    {"label": "Tier 1 (< 0.009ms)", "count": 11032, "percentage": 7.91},
    {"label": "Tier 2 (0.009 - 0.1ms)", "count": 93433, "percentage": 66.96},
    {"label": "Tier 3 (0.1 - 1ms)", "count": 22286, "percentage": 15.97},
    {"label": "Tier 4 (1 - 10ms)", "count": 4703, "percentage": 3.37},
    {"label": "Tier 5 (10 - 50ms)", "count": 4671, "percentage": 3.35}
  ],
  "unbound": {
    "status": "active",
    "total": 37129,
    "hits": 27513,
    "miss": 9616,
    "prefetch": 16437,
    "ratio": 74.10,
    "memory": {
        "msg": { "used_mb": 3.29, "limit_mb": 64.00, "percent": 5.13 },
        "rrset": { "used_mb": 3.51, "limit_mb": 128.00, "percent": 2.74 }
    },
    "cache_count": {
        "messages": 288,
        "rrsets": 486
    }
  }
}