Python SNMP Monitoring — Deep Dive

System-level framing

SNMP monitoring at scale means polling hundreds or thousands of network devices every few minutes, processing counter deltas, detecting anomalies, and routing alerts. Python’s PySNMP library provides a complete SNMP implementation — not just a wrapper around C libraries — giving full control over protocol behavior, MIB resolution, and async operation.

This deep dive uses pysnmp-lextudio, the actively maintained fork of PySNMP.

Installation

pip install pysnmp-lextudio

For MIB compilation (converting vendor MIB files to Python):

pip install pysmi-lextudio

Basic GET operation

from pysnmp.hlapi import (
    getCmd, SnmpEngine, CommunityData, UdpTransportTarget,
    ContextData, ObjectType, ObjectIdentity,
)

iterator = getCmd(
    SnmpEngine(),
    CommunityData("public", mpModel=1),  # mpModel=1 for SNMPv2c
    UdpTransportTarget(("192.168.1.1", 161), timeout=5, retries=2),
    ContextData(),
    ObjectType(ObjectIdentity("1.3.6.1.2.1.1.1.0")),  # sysDescr
    ObjectType(ObjectIdentity("1.3.6.1.2.1.1.3.0")),  # sysUpTime
)

error_indication, error_status, error_index, var_binds = next(iterator)

if error_indication:
    print(f"Error: {error_indication}")
elif error_status:
    print(f"SNMP error: {error_status.prettyPrint()} at {error_index}")
else:
    for var_bind in var_binds:
        print(f"{var_bind[0].prettyPrint()} = {var_bind[1].prettyPrint()}")

SNMPv3 with authentication and encryption

from pysnmp.hlapi import (
    getCmd, SnmpEngine, UsmUserData, UdpTransportTarget,
    ContextData, ObjectType, ObjectIdentity,
    usmHMACMD5AuthProtocol, usmDESPrivProtocol,
    usmHMACSHAAuthProtocol, usmAesCfb128Protocol,
)

# SNMPv3 with SHA auth and AES encryption
iterator = getCmd(
    SnmpEngine(),
    UsmUserData(
        "monitoring-user",
        authKey="auth-passphrase-min-8-chars",
        privKey="priv-passphrase-min-8-chars",
        authProtocol=usmHMACSHAAuthProtocol,
        privProtocol=usmAesCfb128Protocol,
    ),
    UdpTransportTarget(("192.168.1.1", 161)),
    ContextData(),
    ObjectType(ObjectIdentity("1.3.6.1.2.1.1.1.0")),
)

error_indication, error_status, error_index, var_binds = next(iterator)

SNMPv3 security levels:

  • noAuthNoPriv — Username only (not recommended)
  • authNoPriv — Authentication but no encryption
  • authPriv — Both authentication and encryption (recommended)

Bulk operations for efficient polling

GET-BULK retrieves many rows at once, critical for interface tables on devices with hundreds of ports:

from pysnmp.hlapi import (
    bulkCmd, SnmpEngine, CommunityData, UdpTransportTarget,
    ContextData, ObjectType, ObjectIdentity,
)

# Walk the interface table in bulk
for error_indication, error_status, error_index, var_binds in bulkCmd(
    SnmpEngine(),
    CommunityData("public", mpModel=1),
    UdpTransportTarget(("192.168.1.1", 161)),
    ContextData(),
    0,   # nonRepeaters
    25,  # maxRepetitions — rows per request
    ObjectType(ObjectIdentity("1.3.6.1.2.1.2.2.1.2")),   # ifDescr
    ObjectType(ObjectIdentity("1.3.6.1.2.1.2.2.1.10")),  # ifInOctets
    ObjectType(ObjectIdentity("1.3.6.1.2.1.2.2.1.16")),  # ifOutOctets
    lexicographicMode=False,
):
    if error_indication:
        print(f"Error: {error_indication}")
        break
    elif error_status:
        print(f"SNMP error: {error_status.prettyPrint()}")
        break
    else:
        for var_bind in var_binds:
            print(f"{var_bind[0].prettyPrint()} = {var_bind[1].prettyPrint()}")

The maxRepetitions parameter controls how many rows are returned per packet. Tuning this value reduces round trips — 25 is a reasonable default; go higher for large tables on reliable networks.

Async polling with asyncio

For polling many devices concurrently:

import asyncio
from pysnmp.hlapi.asyncio import (
    getCmd, SnmpEngine, CommunityData, UdpTransportTarget,
    ContextData, ObjectType, ObjectIdentity,
)

async def poll_device(engine: SnmpEngine, host: str, community: str) -> dict:
    error_indication, error_status, error_index, var_binds = await getCmd(
        engine,
        CommunityData(community, mpModel=1),
        UdpTransportTarget((host, 161), timeout=5, retries=1),
        ContextData(),
        ObjectType(ObjectIdentity("1.3.6.1.2.1.1.3.0")),  # sysUpTime
        ObjectType(ObjectIdentity("1.3.6.1.2.1.1.5.0")),  # sysName
    )

    if error_indication:
        return {"host": host, "error": str(error_indication)}
    elif error_status:
        return {"host": host, "error": error_status.prettyPrint()}
    else:
        return {
            "host": host,
            "uptime": var_binds[0][1].prettyPrint(),
            "name": var_binds[1][1].prettyPrint(),
        }

async def poll_fleet(hosts: list[str], community: str = "public", concurrency: int = 50):
    engine = SnmpEngine()
    semaphore = asyncio.Semaphore(concurrency)

    async def limited(host):
        async with semaphore:
            return await poll_device(engine, host, community)

    results = await asyncio.gather(*[limited(h) for h in hosts])
    return results

# Usage
hosts = [f"192.168.1.{i}" for i in range(1, 255)]
results = asyncio.run(poll_fleet(hosts))
for r in results:
    if "error" not in r:
        print(f"{r['host']}: {r['name']} (up {r['uptime']})")

Counter delta calculation

SNMP counters are cumulative — they increase continuously and eventually wrap. To get rates (bytes per second, packets per second), calculate deltas between polls:

from dataclasses import dataclass
from typing import Optional
import time

@dataclass
class CounterState:
    value: int
    timestamp: float

class DeltaTracker:
    def __init__(self, counter_max: int = 2**32):
        self._state: dict[str, CounterState] = {}
        self._counter_max = counter_max

    def update(self, key: str, value: int) -> Optional[float]:
        now = time.monotonic()
        prev = self._state.get(key)
        self._state[key] = CounterState(value=value, timestamp=now)

        if prev is None:
            return None  # First reading — no delta yet

        elapsed = now - prev.timestamp
        if elapsed == 0:
            return 0.0

        if value >= prev.value:
            delta = value - prev.value
        else:
            # Counter wrapped
            delta = (self._counter_max - prev.value) + value

        return delta / elapsed  # Rate per second

Use 64-bit counters (HC counters, OID 1.3.6.1.2.1.31.1.1.1.6 for ifHCInOctets) on interfaces faster than 100 Mbps to avoid frequent wraps.

SNMP trap receiver

Devices can send unsolicited notifications (traps) when events occur:

from pysnmp.carrier.asyncio.dgram import udp
from pysnmp.entity import engine, config
from pysnmp.entity.rfc3413 import ntfrcv
import asyncio

snmp_engine = engine.SnmpEngine()

# Configure transport — listen on UDP port 162
config.addTransport(
    snmp_engine,
    udp.domainName,
    udp.UdpTransport().openServerMode(("0.0.0.0", 162)),
)

# Configure community for SNMPv2c traps
config.addV1System(snmp_engine, "my-area", "public")

def trap_handler(snmp_engine, state_reference, context_engine_id,
                  context_name, var_binds, cb_ctx):
    print("Trap received:")
    for oid, val in var_binds:
        print(f"  {oid.prettyPrint()} = {val.prettyPrint()}")

ntfrcv.NotificationReceiver(snmp_engine, trap_handler)

print("Listening for traps on port 162...")
snmp_engine.transportDispatcher.jobStarted(1)

try:
    snmp_engine.transportDispatcher.runDispatcher()
except KeyboardInterrupt:
    snmp_engine.transportDispatcher.closeDispatcher()

Common traps to handle: link up/down, authentication failure, cold/warm start, and vendor-specific events.

MIB resolution

Numeric OIDs are hard to read. MIB files map them to human-readable names:

from pysnmp.hlapi import ObjectIdentity
from pysnmp.smi import builder

# Use named OIDs instead of numeric
obj = ObjectIdentity("IF-MIB", "ifDescr", 1)
obj.resolveWithMib(builder.MibBuilder())

For vendor MIBs, compile them with mibdump.py (from pysmi-lextudio):

mibdump.py --mib-source=./vendor-mibs CISCO-PROCESS-MIB

Production architecture

A scalable SNMP monitoring system has these components:

  1. Device inventory — Database of hosts, SNMP credentials, and poll intervals.
  2. Poller — Async Python process that cycles through the inventory, queries each device, and publishes results.
  3. State store — Redis or similar for counter state between poll cycles.
  4. Alert evaluator — Compares metrics to thresholds and generates alerts.
  5. Time-series store — InfluxDB, Prometheus, or similar for historical data and dashboards.
[Inventory DB] → [Async Poller] → [Redis State]

              [Alert Evaluator] → [PagerDuty / Slack]

              [InfluxDB / Prometheus] → [Grafana]

Tradeoffs

ApproachProsCons
PySNMP (pure Python)Full protocol control, async support, MIB compilationSlower than C libraries, complex API
easysnmp (net-snmp wrapper)Fast, simple APIRequires net-snmp C libraries, less flexible
subprocess + snmpwalkQuick scripts, familiar CLIFragile parsing, slow for bulk
Prometheus SNMP ExporterIntegrates with existing PrometheusConfiguration-heavy, less custom logic

Security considerations

  1. SNMPv2c community strings travel in plaintext — treat them like passwords, use SNMPv3 on untrusted networks.
  2. Restrict SNMP access by IP on device ACLs — only allow your monitoring subnet.
  3. Use read-only community strings for monitoring — never give monitoring tools write access.
  4. Rotate SNMPv3 credentials on a schedule, just like SSH keys.
  5. Monitor for unauthorized SNMP scanning — unexpected SNMP traffic may indicate reconnaissance.

One thing to remember: SNMP monitoring is a polling loop — ask devices questions on a schedule, compute deltas from counters, and alert on anomalies. Python’s async capabilities let you scale this pattern from a handful of devices to thousands, but the real expertise is in knowing which OIDs matter and what the numbers mean.

pythonnetworkingmonitoring

See Also

  • Python Dns Resolver Understand how Python translates website names into addresses, like a phone book for the entire internet.
  • Python Dpkt Packet Parsing Understand how Python reads and decodes captured network traffic, like opening envelopes to see what is inside each message.
  • Python Ftp Sftp Transfers Understand how Python moves files between computers over a network, like a digital delivery truck with a locked or unlocked cargo door.
  • Python Impacket Security Tools Understand how Python speaks the secret languages of Windows networks, helping security teams find weaknesses before attackers do.
  • Python Netconf Yang Understand how Python configures network devices automatically, like a remote control for every router and switch in your building.