Skip to content

The RouterOS API tier

Beside the kernel tier it pulls from the agent, forward can hold one persistent session to the RouterOS binary API for the API tier (a second one when the kernel tier itself comes through the relay) and read what the container cannot see. This page answers what that tier reads, which of it the agent already covers, how to choose how much of it to run, and what a missing value means.

What the agent already reads, and what it cannot

Section titled “What the agent already reads, and what it cannot”

Most of what the API tier can fetch, the agent reads itself:

  • /system/resource and /system/resource/cpu are RouterOS’s one-second average of the same /proc/stat jiffies the agent differences at 10 Hz.
  • /system/health’s temperature is /sys/class/thermal, readable from the container.
  • The conntrack count is the global nf_conntrack slab cache, which the agent reads under privileged=yes; see conntrack without the API.

What is left is per-interface bytes and packets. They live in the router’s network namespace and stay out of reach whatever the container is given; see the router’s CPU, the container’s network.

/system/resource reports cpu-load as an integer percent, and this page calls it a one-second average. That is measured, not assumed: the API series was correlated against the agent’s own per-core busy ratio, which is the same /proc/stat jiffies read at 10 Hz, over two separate hours.

The best fit is a 1.0 s trailing mean with 0.6 s of delay, r = 0.9825 over 3 499 API samples, and 1.1 s with 0.1 s of delay, r = 0.9734 over 3 594 in the second hour. Widening the window only makes the fit worse — 1.5 s gives 0.955, 2 s gives 0.919, 5 s gives 0.822, 8 s gives 0.791.

A sixty-second average is ruled out twice over. Its correlation is 0.238, and the step response has no ramp in it: at the sharpest load step in the day the kernel went from 5 % to 27 % in one second and cpu-load went from 5 to 26 in that same second, then from 22 % to 6 % on the way down just as fast. A one-minute average would have needed a minute to travel either way.

Measured on RB5009UG+S+ · 4 × 1.4 GHz Cortex-A72 · RouterOS 7.24.2 · Linux 5.6.3 · · /system/resource polled at 1 Hz against the agent's per-core busy ratio at 10 Hz, two separate hours

So the number is what it claims to be. What it still cannot do is resolve anything shorter than its own second, which is the whole reason this project reads the kernel.

--api-mode picks a preset. It is full unless you say otherwise, and an explicit --api-every, --no-health or --conntrack-every still wins over it.

Mode What it does Use it when
off no API tier: --api-every 0 production, when per-interface traffic already comes from somewhere else
slow one round every 10 s, without /system/health and without the conntrack count; /system/resource, monitor-traffic and the port counters still run on each round production, when you want interface rates too
full one round every second, with /system/health; port counters every 10 s; the conntrack count only if --conntrack-every asks for it experiments and record runs

off is the mode that gives per-interface traffic up; slow is the one that keeps it cheaply. Two dashboard panels are blank by configuration under slow rather than by device — /system/health and the RouterOS connection count — and they name the flag that fills them.

Every API-tier command runs on the tier’s one session, one at a time, with a 15 s timeout each. The slower cadences are checked on each round, so none of them runs more often than --api-every.

Command Asks for Runs Off when
/system/resource/print cpu-load, free-memory, total-memory, free-hdd-space, uptime, version every round the tier is off
/system/resource/cpu/print cpu, load, irq, disk every round the tier is off
/system/health/print name, value every round --no-health, or slow
/interface/monitor-traffic with interface=<list> and once the four traffic rates and whichever loss rates the router returns every round, one call for all interfaces --interfaces is empty
/interface/print with .proplist=name,default-name,type,comment,actual-mtu what each interface is: its current name, the board’s name for it, its type, its comment and its MTU at start, before the first kernel pull, then every --labels-every (5 min) the tier is off
/interface/list/member/print with .proplist=list,interface which interface lists name each interface, which is its role with the read above the tier is off
/interface/bridge/port/print with .proplist=interface,bridge which bridge each port belongs to with the read above the tier is off
/interface/ethernet/print stats and /interface/print stats-detail every numeric counter of every interface every --counters-every (10 s) --counters-every 0
/ip/firewall/connection/print count-only the connection count every --conntrack-every --conntrack-every 0, the default

A command that fails leaves its part of the sample empty and records why; the rest of the round still stands. The failures are logged on standard error as api tier: …, written as lines to Loki and rows to SQL, and counted on OTLP — never turned into a value. An /interface read that fails keeps the inventory already held — a transient error does not blank every panel’s label — and is reported as inventory: …; the list and bridge reads are best effort, and without them the inventory still carries names, types and comments.

The API-tier sample is stamped with the collector’s clock plus the skew measured against the agent, so it lands on the agent’s timeline.

The three inventory reads answer what the counters cannot: what the numbers belong to. Per interface they give its current name, the board’s default name (the factory ether5 of a physical port, empty for a bridge, a VLAN or a tunnel), RouterOS’s own type (ether, bridge, vlan, pppoe-out, wg, veth, loopback), the comment, the interface lists it belongs to — sorted and comma-joined, WAN or LAN,VPN — the bridge it is a port of, and the MTU. A bridge member that is in no list of its own takes its bridge’s lists, because that is how a RouterOS firewall rule matches it, and a bridge port that names an interface list rather than an interface is not labelled.

On the reference RB5009 (RouterOS 7.24.2, 2026-09-16) the three reads return 17 interfaces. ether1 is an ether in the LAN list, a port of bridge, labelled “TrueNAS - High Performance Storage”, MTU 9000; ether5 is an ether in WAN and in no bridge, labelled “DIGI ONT”; PPPoE_DIGI is pppoe-out, WAN, MTU 1480; VLAN_DIGI is vlan, WAN; wg_devices and wg_trastero are wg in LAN,VPN; ether6 and ether7 carry the comment “Unused”.

This is configuration, not telemetry, so it is read once at start — before the first kernel pull, so a kernel-log record is labelled from the first line — and then on the slow --labels-every cadence, never per poll. An edited comment, or a port moved between lists, reaches the dashboard within minutes rather than at the next collector restart, and a comment removed in RouterOS disappears here too: the read replaces the inventory wholesale. None of the five properties it asks for can carry a secret.

Every monitor-traffic rate row and every per-port counter row then carries label (the comment), type, role and bridge, so a panel says what is plugged in rather than a port number, and says which numbers may be compared. Prometheus is the exception by design: a comment is edited by a human, and a changing label would spawn a new series for every edit, so the collector’s /metrics carries one info series per interface — mikroscope_api_interface_info{interface,label,type,role,bridge,default_name} 1, for every interface whether or not it has a comment — and a query joins it:

mikroscope_api_interface_counter_total * on(interface) group_left(label, type, role) mikroscope_api_interface_info

The inventory also gives the kernel log its names. A kernel record names a port as eth1, the board’s port table maps that to the RouterOS default name, and the inventory maps the default name to the current one, so an operator who renamed ether5 to WAN reads WAN on the panel, with the port’s label and role beside it. Without the API tier a kernel record keeps the board’s default name and gets no label.

Flag Default Meaning
--api host:port MIKROSCOPE_API_ADDR the RouterOS binary API, e.g. 192.168.88.1:8728
--api-user MIKROSCOPE_API_USER the API user; its password only from MIKROSCOPE_API_PASSWORD
--api-mode full off, slow or full, as above
--api-every 1s the round’s cadence; 0 disables the tier
--interfaces MIKROSCOPE_INTERFACES, empty comma-separated interfaces for monitor-traffic, e.g. bridge,ether1
--counters-every 10s how often to read every port’s cumulative counters; 0 never
--labels-every 5m how often to re-read what each interface is — comment, type, interface lists, bridge; 0 means the default
--conntrack-every 0 how often to ask the connection count; 0 never, because it is a table scan
--no-health off skip /system/health

Without --api, --api-user and MIKROSCOPE_API_PASSWORD, or when the session cannot be opened within 10 s, forward logs api tier disabled: … and runs the kernel tier alone. That is a warning, not a failure: the kernel tier is the point.

In any mode the API tier’s commands are reads: a user with the read and api policies is enough. test is needed only for /tool fetch, which the relay transport and nothing else in the API tier uses. The group, the address restriction and why the credential never lives on the router are on the API user.

The API tier keeps exactly what the router returned and invents nothing for what it did not.

Loss rates. monitor-traffic may return rx-drops, tx-drops, tx-queue-drops, rx-errors and tx-errors per second, and a sink writes each one only when it came back. On the RB5009 with RouterOS 7.24.2 (2026-09-15) it returns the three drop rates and no error keys at all.

Port counters. The per-port counters are a map of RouterOS’s own field names, not a fixed set of fields, and only plain unsigned integers that count something are kept. mtu, actual-mtu, l2mtu, max-l2mtu and sfp-shutdown-temperature parse as integers but are sizes and configuration, and every sink renders this map as a counter family, so they are dropped rather than left for each consumer to know about; the MTU travels in the inventory instead. The two commands are merged per interface: /interface/ethernet/print stats brings the MAC’s typed errors, the collision family, the frame-size buckets and the driver counters; /interface/print stats-detail brings the fast-path fp-* counters, link-downs, tx-queue-drop and the kernel-side totals. A board without collision counters produces no collision entries rather than a row of zeroes that reads as “no collisions”.

The reason is a measurement. On 2026-09-15 the reference router’s ether1 had 652 364 rx-overflow events, growing, and monitor-traffic returns no error key for that port at all: that count reaches a consumer only through the port counters. What those overflows turned out to be is below.

The counters cover every interface the router lists, not only --interfaces: the port a fault lives on is often one nobody thought to monitor, and the layer-2 loop of 2026-09-12 was on a port absent from the monitor-traffic list.

They are cumulative since boot or the port’s last reset, so a consumer differences them. The derivation they enable, measured on ether1 (2.5 GbE to a NAS, 2026-09-16, since the port’s last counter reset): 255.8 GB of rx-bytes on the wire, of which 29.7 GB of driver-rx-byte reached the CPU — the rest the switch chip forwarded in hardware, and no counter inside the container has a number for it. The collector turns the fast-path counters beside them into a share of the traffic each interface hands the CPU.

A switch port and a bridge count different things, which is why the type travels with every row. An ether in a bridge counts its wire, including the frames the switch chip forwarded without the CPU; the bridge counts its own CPU side; a VLAN or a PPPoE link counts what the CPU sent and received. ether1 and bridge are two planes, neither a subset of the other: drawn side by side without their type they read as peers, and summed they double-count. Never add them.

Interface labels. What each row is labelled with — the comment, the type, the role and the bridge — is the inventory above, and an interface the inventory does not list carries none of them rather than an invented blank identity.

The port counters are the only place the reference router’s ether1 overflows appear, and over fifteen hours they say what kind of event they are. Measured 2026-09-15 between 07:13 and 22:20 UTC, over 4 471 consecutive 10 s counter polls of ether1 (2.5 Gbps to a NAS, MTU 9000):

  • 126 443 rx-overflow events in all, present in 40 % of the intervals; per interval the median is 29, the p99 about 1 036 and the largest 3 747. Over the run that is 0.53 % of the packets the NAS sent.
  • Rank correlation over the 10 s deltas: 0.85 against the part of the NAS’s receive the switch forwarded in hardware (rx-bytes minus driver-rx-byte), 0.00 against the part it sent to the CPU (driver-rx-byte).
  • Where it was going: ether8 (NGINX, 1 Gbps) carries most of the volume, ether4 (Mastodon, 1 Gbps) is the most frequent destination; the SFP+ cage, ether2, ether3 and the CPU path show nothing.
  • The frames were large: the 1024-and-up frame-size bucket on ether1 has a median of 9 331 per interval with overflow against 1 336 per interval without.
  • The load was not high: the median NAS receive in an interval with overflow is about 9 Mbit/s as a 10 s mean. Bursts, not sustained load.
  • Nothing on the CPU side: softnet dropped 0, time_squeeze correlates 0.04 and the switch0 interrupts 0.05 against the overflows, with the agent’s 10 Hz data binned to 10 s. No pause frames on ether1 in either direction.

Read together that is consistent with 2.5 Gbps line-rate bursts switched inside the chip toward 1 Gbps ports with no flow control in effect. Not verified: the switch chip’s exact counter semantics, and the NAS’s own retransmit count — the counters are the port’s, not the conversation’s.

The kernel tier cannot see any of it by construction. A frame the switch chip forwards in hardware never reaches the CPU, so no /proc file on the router has a number for it; it takes the per-port counters, and only the API has those.

Measured on the reference RB5009 (RouterOS 7.24.2, 2026-09-16) with /tool profile duration=60s cpu=total, once with the collector stopped and once with it running --interfaces bridge,ether1,PPPoE_DIGI --counters-every 10s --api-every 1s. The first five one-second snapshots of each profile are dropped: they hold the SSH connect that asked for it.

Profile row Collector stopped Collector running
total 5.93 % 6.04 %
interface-mgmt 0.40 % 0.87 %
config-db about 0 % 0.15 %

The total moves by 0.11 points, which is inside the traffic noise of a minute; the two rows that answer the tier’s questions move by about half a point between them. No api process row appears in either profile: the API process relays, and the work lands on the subsystem that answers. So a tier running a round every second costs the router about 0.5 % of its total CPU.

That is small, and the project still treats the API as the costly path. Per-port data comes from the container wherever the container can see it, and configuration is read at start and on the slow labels cadence, never per poll.

--conntrack-every 10s asks /ip/firewall/connection/print count-only at that cadence: 1.3 ms at 6 212 entries on the RB5009 (date not recorded). It is off by default because it is a table scan over an API session, and under privileged=yes the agent’s nf_conntrack slab count is the same population read from a file at the sampler’s rate. The two do not match exactly — they are sampled at different instants, and the slab counts objects the allocator still holds — but they track: the API said 6 212 the day before the slab said 6 287.