Skip to main content

Detect blocking calls in asyncio event loops using eBPF + Austin

Project description

blocksnoop

Detect blocking calls in Python asyncio event loops using eBPF + Austin.

blocksnoop attaches to a running Python process (or launches one) and reports every time the event loop is blocked longer than a configurable threshold — with the Python stack trace that caused it.

How it works

eBPF (kernel)          Austin (userspace)
  │ monitors               │ samples Python
  │ epoll gaps              │ stacks continuously
  └──────────┐   ┌─────────┘
             ▼   ▼
          Correlator
             │
             ▼
       Reporter → sinks (console, JSON, file)
  1. An eBPF probe hooks epoll_wait syscalls and measures the time between returns (callback start) and the next entry (callback end). If the gap exceeds the threshold, it emits an event.
  2. A stack sampler (Austin) runs as a long-lived subprocess, continuously streaming Python stack traces into a ring buffer. Austin's pipe mode avoids per-sample subprocess overhead, enabling sub-10ms threshold detection.
  3. The correlator enriches each blocking event with the closest matching Python stack.
  4. The reporter fans out events to one or more output sinks.

Requirements

Installation

pip install blocksnoop

Or for development:

git clone git@github.com:PaulM5406/blocksnoop.git
cd blocksnoop
uv sync --all-extras --dev

Usage

Attach to a running process

sudo blocksnoop <PID>
sudo blocksnoop -t 50 <PID>          # 50ms threshold (default: 100ms)
sudo blocksnoop --tid 1234 <PID>     # monitor specific thread
sudo blocksnoop -v <PID>             # enable debug logging

Launch and monitor a process

sudo blocksnoop -- python app.py
sudo blocksnoop -t 50 -- python app.py

Output modes

# Human-readable to stderr (default)
sudo blocksnoop -- python app.py

# JSON lines to stdout (for piping to jq, etc.)
sudo blocksnoop --json -- python app.py

# Structured JSON to file (for Datadog/Fluentd/CloudWatch)
sudo blocksnoop --log-file /var/log/blocksnoop/events.json --service my-api --env production -- python app.py

# Combine: console to terminal + JSON to file
sudo blocksnoop --log-file /var/log/blocksnoop/events.json --service my-api -- python app.py

Example output

Human-readable:

[   1.23s] #1   BLOCKED     302.1ms  tid=1234
  Python stack (most recent call last):
    app.py:7 in blocking_io
    app.py:13 in main

[   2.05s] #2   BLOCKED     298.5ms  tid=1234
  Python stack (most recent call last):
    app.py:7 in blocking_io
    app.py:13 in main

--- blocksnoop session ---
Duration: 8.0s
Blocking events detected: 2

JSON (--json):

{"event_number": 1, "timestamp_s": 1.23, "duration_ms": 302.1, "pid": 5678, "tid": 1234, "python_stacks": [[{"function": "blocking_io", "file": "app.py", "line": 7}, {"function": "main", "file": "app.py", "line": 13}]], "level": "warning"}

CLI reference

blocksnoop [OPTIONS] [PID] [-- COMMAND ...]

Options:
  -t, --threshold FLOAT        Blocking threshold in ms (default: 100)
  --tid INT                    Thread ID to monitor (default: main thread)
  --json                       JSON lines output to stdout
  --log-file PATH              Write structured JSON to file for log aggregators
  --service NAME               Service name for structured logs (default: blocksnoop)
  --env ENV                    Environment tag for structured logs
  --no-color                   Disable ANSI colors in terminal output
  -v, --verbose                Enable debug logging to stderr
  --error-threshold MS         Duration in ms above which events are errors (default: 500)
  --correlation-padding MS     Correlation time window padding in ms (default: 200)

Docker

blocksnoop requires kernel access, so Docker containers need --privileged and --pid=host:

# Pull from Docker Hub
docker pull oloapm/blocksnoop

# Attach to a process on the host
docker run --rm --privileged --pid=host oloapm/blocksnoop blocksnoop -t 100 <PID>

# Launch and monitor a process
docker run --rm --privileged --pid=host oloapm/blocksnoop blocksnoop -t 100 -- python app.py

For local development:

# docker-compose.yml
services:
  blocksnoop:
    build: .
    privileged: true
    pid: host
docker compose run --rm blocksnoop blocksnoop -t 100 -- python app.py

Kubernetes

blocksnoop uses eBPF which operates at the kernel level, so you run it on the node, not inside the application container. The target process just needs to be visible from the host PID namespace.

Ephemeral debug container (recommended)

Attach directly to a running pod with an ephemeral container. --profile=sysadmin (K8s 1.28+) grants the privileged access required for eBPF:

# Find the pod
kubectl get pods -l app=my-api

# Attach an ephemeral debug container with eBPF privileges
kubectl debug -it my-api-pod-7b8c9d \
  --image=oloapm/blocksnoop:latest \
  --target=my-api \
  --profile=sysadmin \
  -- sh -c "mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null; exec sh"

--target shares the process namespace with the app container, so you can see its PIDs. --profile=sysadmin enables privileged mode for eBPF and debugfs access.

Inside the debug container, find the Python process and attach:

# Find the Python PID
ps aux | grep python

# Attach blocksnoop
blocksnoop -t 50 <PID>

# Or with structured logging
blocksnoop --json -t 50 <PID>

blocksnoop automatically detects and symlinks available kernel headers when the running kernel differs from the installed headers package (common in containers).

DaemonSet sidecar

For continuous monitoring, deploy blocksnoop as a DaemonSet that monitors processes on each node:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: blocksnoop
spec:
  selector:
    matchLabels:
      app: blocksnoop
  template:
    metadata:
      labels:
        app: blocksnoop
    spec:
      hostPID: true
      containers:
        - name: blocksnoop
          image: oloapm/blocksnoop:latest
          command: ["blocksnoop", "--json", "--log-file", "/var/log/blocksnoop/events.json", "--service", "my-api", "--env", "production", "-t", "100"]
          securityContext:
            privileged: true
          volumeMounts:
            - name: logs
              mountPath: /var/log/blocksnoop
            - name: debugfs
              mountPath: /sys/kernel/debug
      volumes:
        - name: logs
          hostPath:
            path: /var/log/blocksnoop
        - name: debugfs
          hostPath:
            path: /sys/kernel/debug

The log file at /var/log/blocksnoop/events.json can be tailed by Datadog Agent, Fluentd, or any log collector running on the node.

Node shell (quick one-off)

For a quick check without building images:

# SSH into the node (or use a node shell tool)
kubectl node-shell <node-name>

# Install blocksnoop
pip install blocksnoop

# Find the Python process (hostPID shows all processes)
ps aux | grep python

# Attach
blocksnoop -t 50 <PID>

Development

# Install dependencies
uv sync --all-extras --dev

# Run unit tests
uv run --extra dev pytest tests/ -v --ignore=tests/integration

# Run integration tests (requires Docker)
uv run --extra dev pytest -m docker tests/integration/ -v

# Lint and format
ruff check blocksnoop/ tests/
ruff format blocksnoop/ tests/

# Type check
ty check blocksnoop/

License

GPL-3.0-or-later (due to the austin-python dependency)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

blocksnoop-0.3.0.tar.gz (39.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

blocksnoop-0.3.0-py3-none-any.whl (30.4 kB view details)

Uploaded Python 3

File details

Details for the file blocksnoop-0.3.0.tar.gz.

File metadata

  • Download URL: blocksnoop-0.3.0.tar.gz
  • Upload date:
  • Size: 39.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for blocksnoop-0.3.0.tar.gz
Algorithm Hash digest
SHA256 cade6ce5179860efa43205ff13f08288b78381d2b086996f0d5480878beec700
MD5 64502b8c951a00d9ef7389cb09435d04
BLAKE2b-256 2e356daae029f173f1bb819a3c23dbdfd7f7377b8d337909dd018a05018d8ef1

See more details on using hashes here.

Provenance

The following attestation bundles were made for blocksnoop-0.3.0.tar.gz:

Publisher: release.yml on PaulM5406/blocksnoop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file blocksnoop-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: blocksnoop-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 30.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for blocksnoop-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 19b1edede3382899b81af31212f4c58def1bb9b5aafb78e56350da9321e836db
MD5 7ddc8dddd9c6fe88d39e1fb6c188ecfb
BLAKE2b-256 3884ecd14cf74b298ea49d8d77731e95b714eaef45c9632bc2bfbd13e7300608

See more details on using hashes here.

Provenance

The following attestation bundles were made for blocksnoop-0.3.0-py3-none-any.whl:

Publisher: release.yml on PaulM5406/blocksnoop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page