Understanding MIBs
A MIB is more than a list of names and numbers - it's a formally structured document, written to a strict grammar, where every queryable value, every table, and every event a device can report is defined using one of a small number of standard building blocks. This guide covers those building blocks in depth: how a single data point gets defined, how tables and their indexing actually work, how rows get created and deleted, how notifications are formally specified, the grammar underneath all of it, and how vendor-specific numbering keeps everyone's private definitions from colliding.
OBJECT-TYPE: defining a single data point
OBJECT-TYPE is the macro used to define a single manageable data point - almost everything you can query over SNMP is defined this way. If a MIB has a hundred queryable values, it has roughly a hundred OBJECT-TYPE definitions, one per value.
ifDescr OBJECT-TYPE
SYNTAX DisplayString (SIZE (0..255))
MAX-ACCESS read-only
STATUS current
DESCRIPTION
"A textual string containing information about the
interface."
::= { ifEntry 2 }
Every field here is doing real work:
- SYNTAX - the data type. Could be a basic ASN.1 type (
INTEGER,OCTET STRING) or a textual convention likeDisplayStringthat adds meaning and formatting on top of a basic type. - MAX-ACCESS - whether this can only be read, or also written via SET. Common values:
read-only,read-write,read-create(for table rows creatable via SET - see RowStatus), andnot-accessible(objects that exist structurally, like a table's own row entry, but aren't themselves queryable). - STATUS -
current,deprecated(still works, but shouldn't be used in new implementations), orobsolete(no longer expected to be supported). - DESCRIPTION - a plain-English explanation. Often the single most useful field for a human trying to understand an unfamiliar OID.
::= { ifEntry 2 }- the object's position in the OID tree, defined relative to its parent (here, the 2nd child ofifEntry) rather than as a full absolute number - the actual numeric OID gets resolved by walking up the whole chain of parent definitions.
An OBJECT-TYPE can define either a standalone scalar value (exactly one instance, queried with a trailing .0) or a column within a table (queried with an instance index - see tables and indexing below). The definition itself doesn't look dramatically different either way; what makes something a table column is really whether its parent in the OID tree is a SEQUENCE-typed row entry, itself part of a larger table structure.
Because every queryable object follows this same structured format, MIB-parsing tools (including MIB Viewer) can reliably extract a name, data type, access level, and description for any object, across every vendor and every MIB, without custom logic per MIB. That consistency is what makes MIB-based tooling possible at all - a shared, machine-parseable format underneath what would otherwise just be vendor-specific documentation.
Textual conventions: reusable, meaningful types
A textual convention is a named, reusable data type built on top of one of SNMP's basic ASN.1 types, adding a specific meaning, format, or constraint the basic type alone doesn't capture. Rather than every MIB re-describing "this is a 32-bit integer representing a timestamp, sort of" in prose each time, a textual convention gives that pattern a name and a precise definition once, reused consistently everywhere it applies. The ones you'll actually run into constantly:
- DisplayString - an
OCTET STRINGrestricted to printable ASCII, used for human-readable fields likesysDescrorifDescr. - TimeTicks / TimeStamp - a count of hundredths of a second, typically since some reference point like system boot (
sysUpTimeis the canonical example). - Counter32 / Counter64 - a monotonically increasing value that wraps back to zero after its maximum, rather than something that can go up or down - used for things like total bytes transmitted, which only ever increase (or wrap) between polls.
- Gauge32 - similar to a counter, but can go both up and down, and stays pinned at its maximum or minimum rather than wrapping - used for things like current queue depth.
- IpAddress - a 4-byte value representing an IPv4 address.
- RowStatus - see below - controls creating and deleting table rows.
- TruthValue - a simple boolean, encoded as the integers
1(true) or2(false) - notably not0/1, which trips up people coming from most programming languages.
The most common, widely-reused textual conventions live in a small number of foundational MIBs - particularly SNMPv2-TC - that most other MIBs import from. Vendors also frequently define their own additional ones in their private MIBs, for values specific to their hardware.
A textual convention changes how a raw value should actually be understood, not just displayed. A Counter32 that appears to have "gone down" between two polls hasn't necessarily done anything wrong - it may have simply wrapped past its maximum, which is expected, normal behavior for a counter specifically, but would be a real anomaly for a gauge. Treating a wrapping counter as an ordinary integer produces misleading results; deriving a rate from two counter samples requires accounting for the possibility of a wrap in between them. In an OBJECT-TYPE's SYNTAX field, you'll often see a convention name directly instead of a raw ASN.1 type - SYNTAX DisplayString rather than a bare OCTET STRING with a size constraint spelled out inline - because the convention carries its full meaning and formatting rules along with its name.
Tables and indexing
SNMP tables - repeating data like one row per network interface, one row per routing entry - use an INDEX clause to define how a specific row gets identified. Understanding indexing is what turns "table columns are confusing OIDs with extra numbers on the end" into something that actually makes sense.
The classic example is ifTable, indexed by ifIndex:
ifEntry OBJECT-TYPE
...
INDEX { ifIndex }
::= { ifTable 1 }
This means: to identify a specific row, you append the value of ifIndex for that row to the end of any column's OID. If interface 1 has ifIndex value 1, then ifDescr for that interface is at 1.3.6.1.2.1.2.2.1.2.1 - the column's own OID, plus .1 for the row.
Not every table is indexed by a single simple integer. Some are indexed by multiple columns together, and the resulting instance suffix is the concatenation of all of them, in order - a classic example being a table indexed by both an interface index and an IP address (INDEX { ifIndex, ipAddr }). If ifIndex is 1 and the address is 192.168.1.10, the instance suffix becomes 1.192.168.1.10 - the IP address encoded as four separate decimal numbers, one per octet, appended after the interface index. This is exactly why some real-world OIDs have long tails of numbers that look like an IP address hiding in plain sight - because that's often literally what they are.
When a string is used as part of an index, its encoding depends on whether it's declared with the IMPLIED keyword. Without IMPLIED, a variable-length string index is prefixed with its own length (so a parser knows where it ends and the next index component begins); with IMPLIED (only valid on the last index component), the string appears with no length prefix, since being last means there's no ambiguity about where it ends.
Once you understand that the trailing numbers after a column's base OID are an encoded index - not random noise - a long, unfamiliar-looking OID stops being intimidating. 1.3.6.1.2.1.4.22.1.2.1.192.168.1.1 isn't an arbitrary number; it's very likely ipNetToMediaPhysAddress indexed by an interface number (1) and an IP address (192.168.1.1), once you know to look for that pattern. This is also directly relevant to row creation below, since creating a new row means choosing values for every index column as part of that row's own identity - not something changeable afterward.
RowStatus: creating and deleting table rows
RowStatus is a special textual convention used to create and delete rows in certain tables via SET operations - rather than a row simply existing because the device's own configuration created it, some tables are specifically designed to let a manager add or remove rows directly over SNMP. This mostly shows up in tables where the manager itself needs to define new entries - a static route, an access control entry, a monitored threshold - not tables that just passively reflect the device's existing state (you can't create a new physical network interface by SETting a row into ifTable).
RowStatus is an enumerated integer with six defined values: active(1) (the row exists and is in use), notInService(2) (exists but deliberately inactive), notReady(3) (exists but missing required information before it can activate - a transitional state, not something you set directly), createAndGo(4) (create and activate in one step), createAndWait(5) (create but leave in notReady until explicitly activated afterward - for creation spread across multiple SET operations), and destroy(6) (delete the row entirely).
To create a row, you SET values for the row's index columns (the index values themselves define which row you're creating, since a row's identity is its index) along with any other required columns and the RowStatus column itself, typically createAndGo for simple, single-step creation. If several columns need to be set before the row is valid and you'd rather not have it briefly active with incomplete data, createAndWait lets you set everything first and activate it as a deliberate final step. A plain SET changes one existing value; row creation via RowStatus is really a small, ordered sequence of operations, and getting the order or values wrong is a common source of confusion - a row stuck in notReady usually means some required column hasn't been set yet, and the agent won't activate an incomplete row on its own. Deletion is more straightforward: SET the row's RowStatus to destroy, and the entire row - not just that one column - is removed.
NOTIFICATION-TYPE: defining events
NOTIFICATION-TYPE is the macro used in SNMPv2c/v3 MIBs to define a specific kind of unsolicited event a device can report - the formal, MIB-level definition underlying an individual trap or inform. Where an OBJECT-TYPE defines a data point you can query, a NOTIFICATION-TYPE defines an event the device can proactively report, along with exactly what data comes bundled with it.
linkDown NOTIFICATION-TYPE
OBJECTS { ifIndex, ifAdminStatus, ifOperStatus }
STATUS current
DESCRIPTION
"A linkDown trap signifies that the SNMP entity,
acting in an agent role, has detected that the
ifOperStatus object for one of its communication
links is about to enter the down state."
::= { snmpTraps 3 }
The OBJECTS clause is the important part: it lists exactly which other OIDs' current values get bundled into the notification when it fires - here, which interface changed, and its admin and operational status. That's what lets a manager receiving a linkDown notification know immediately which interface it was about, without a separate follow-up query. When a v2c/v3 trap or inform is sent, it includes a special varbind, snmpTrapOID, whose value is the OID of the specific NOTIFICATION-TYPE that fired - 1.3.6.1.6.3.1.1.5.3 for the example above. A receiving tool with the right MIB loaded can look that OID up, recognize it as linkDown, and correctly interpret the rest of the bundled varbinds according to that notification's own OBJECTS list.
Like objects, notifications come in both standard, cross-vendor forms (link up/down, cold/warm start, authentication failure - all defined in core MIBs every implementation is expected to support) and vendor-specific ones defined in private, enterprise MIBs - a fan failure, a specific hardware fault code, a license expiring. If a monitoring platform receives a trap whose snmpTrapOID it doesn't have a matching definition for, it can typically still show the raw data - the OID itself, and whatever varbinds came with it - but can't tell you in plain language what happened. That's exactly the same underlying problem as any other unresolved OID: the right MIB simply isn't loaded, or for a genuinely non-existent OID, doesn't exist anywhere at all.
The SMI: the rulebook underneath all of this
The SMI (Structure of Management Information) is the set of rules defining how a MIB is allowed to be written - the formal grammar and base data types every MIB has to follow. If a MIB is a document describing a specific set of manageable data, the SMI is the rulebook that document has to comply with in order to be a valid MIB at all. It's built on ASN.1 (a general-purpose data description language originally developed for telecommunications), with SNMP-specific extensions and restrictions layered on top - the SMI is essentially "the specific, constrained subset of ASN.1 usable for defining MIBs," not a wholly separate language of its own.
Like SNMP itself, the SMI has gone through revisions. SMIv1 (RFC 1155) accompanied SNMPv1; SMIv2 (RFC 2578) came with SNMPv2 and added Counter64 for 64-bit counters, a more expressive mechanism for textual conventions generally, and OBJECT-GROUP/NOTIFICATION-GROUP/MODULE-COMPLIANCE - the machinery behind conformance statements, covered next. Almost every actively-maintained MIB you'll encounter today is written to SMIv2; SMIv1-only MIBs still exist, mostly for very old equipment, but are increasingly rare.
Concretely, the SMI specifies the base types available in a MIB (INTEGER, OCTET STRING, OBJECT IDENTIFIER, and SNMP-specific application types like Counter32/Gauge32), the macros used to define things (OBJECT-TYPE, NOTIFICATION-TYPE, and others), and the rules for how OIDs get assigned relative to their parent in the tree. For most people, the SMI is invisible - you interact with the objects a MIB defines, not the grammar rules that made those definitions valid. It becomes directly relevant in exactly one common situation: a MIB failing to parse. A parser is, among other things, checking SMI compliance, and a MIB that violates its rules (an undefined type, malformed syntax, a macro used incorrectly) fails to load with an error pointing at the specific problem - see Troubleshooting SNMP for what that looks like in practice.
Conformance and compliance
Conformance statements are part of the SMI that let a MIB formally define which of its objects are actually required for an implementation to claim support - as opposed to simply listing every possible object with no indication of which are essential versus optional. This solves a real problem: a large MIB might define dozens of objects, but a specific vendor's hardware might only sensibly support a subset (a feature that doesn't apply to that hardware, an optional capability not yet implemented). Conformance statements say precisely which subset is required, which is optional, and under what conditions - without needing a separate, informal document to explain it.
Objects and notifications are first bundled into logically related groups:
ifGeneralGroup OBJECT-GROUP
OBJECTS { ifIndex, ifDescr, ifType, ifMtu }
STATUS current
DESCRIPTION
"The generic managed objects applicable to all
network interfaces."
::= { ifGroups 1 }
That just names a related set as a unit - it doesn't yet say whether the unit is required. A separate MODULE-COMPLIANCE statement then defines an actual compliance level, referencing which groups are mandatory and which are optional (sometimes with conditions - "required if you support X"):
ifCompliance MODULE-COMPLIANCE
STATUS current
DESCRIPTION "Compliance statement"
MODULE
MANDATORY-GROUPS { ifGeneralGroup, ifCounterDiscontinuityGroup }
::= { ifCompliances 1 }
An implementation supporting every mandatory group referenced by a given compliance statement can claim to meet that specific level - a formal, checkable claim, not just a marketing statement. For anyone consuming a MIB rather than writing one, conformance statements are mostly useful as documentation: a reliable way to tell, just by reading the MIB, which objects a spec-compliant device is expected to support versus which are genuinely optional extras. That's a meaningfully different question from "which objects does this specific device happen to implement," which conformance statements describe the intended baseline for but can't guarantee - real-world implementations don't always fully comply with what they claim. Conformance sections show up near the bottom of most well-structured MIBs, generally after all the actual object and notification definitions.
Enterprise OIDs and private MIBs
The enterprises branch of the OID tree - 1.3.6.1.4.1 - is where every vendor's own, private MIB content lives. Anything hardware-specific that isn't covered by a standard, cross-vendor MIB (a proprietary hardware sensor, a vendor-specific feature, an internal diagnostic counter) gets defined somewhere under this branch, in what's generally called a private or enterprise MIB.
Any organization can request its own unique number directly under enterprises, formally through IANA's Private Enterprise Number registry. Once assigned, that number is that organization's own private namespace to define however it chooses - nobody else will ever be assigned the same number, and no further approval is needed to define new objects underneath it. Some well-known examples: 1.3.6.1.4.1.9 is Cisco, 1.3.6.1.4.1.2636 is Juniper, 1.3.6.1.4.1.6527 is Nokia (formerly Alcatel-Lucent), 1.3.6.1.4.1.30065 is Arista, and 1.3.6.1.4.1.11 is Hewlett-Packard. Everything that vendor defines lives somewhere underneath their assigned number - a Cisco-specific OID will always start with 1.3.6.1.4.1.9, with no ambiguity about which vendor a given enterprise OID belongs to once you recognize the number.
This is the flip side of the standard-vs-vendor distinction that runs through this whole guide: standard MIBs (under mib-2 and similar well-known branches) work identically across every vendor's implementation, while enterprise MIBs are specific to one vendor's hardware and aren't expected to be portable anywhere else. Standard MIBs are widely distributed and commonly bundled with monitoring tools by default; enterprise MIBs are not - there are thousands of registered enterprise numbers, most defining MIBs far less commonly needed, and no single tool ships with all of them preloaded. This is exactly why an unresolved OID so often turns out to be sitting under some vendor's enterprises branch: the object genuinely exists and is well-defined, but the specific MIB that defines it simply isn't loaded wherever you're looking it up. Finding and loading the correct vendor MIB - typically available from that vendor's own support site - is usually the actual fix; see Troubleshooting SNMP for the fuller picture of unresolved-OID situations. IANA maintains the full, public list of assigned Private Enterprise Numbers - if you have an OID starting with 1.3.6.1.4.1. followed by a number you don't recognize, that number is a direct, reliable way to identify which vendor defined it, even before you've found the specific MIB file itself.