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/resourceand/system/resource/cpuare RouterOS’s one-second average of the same/proc/statjiffies 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_conntrackslab cache, which the agent reads underprivileged=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.
What cpu-load actually averages
Section titled “What cpu-load actually averages”/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.
Choosing how much to ask
Section titled “Choosing how much to ask”--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 |
Scroll sideways to see every column
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.
What one round asks
Section titled “What one round asks”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 |
/ |
cpu, load, irq, disk |
every round | the tier is off |
/system/health/print |
name, value |
every round | --no-health, or slow |
/ 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 . |
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 |
/ with .proplist=list,interface |
which interface lists name each interface, which is its role | with the read above | the tier is off |
/ with . |
which bridge each port belongs to | with the read above | the tier is off |
/ and /interface/print stats-detail |
every numeric counter of every interface | every --counters-every (10 s) |
--counters-every 0 |
/ |
the connection count | every --conntrack-every |
--conntrack-every 0, the default |
Scroll sideways to see every column
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.
What each interface is
Section titled “What each interface is”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_,
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_infoThe 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 |
Scroll sideways to see every column
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.
The user it needs
Section titled “The user it needs”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.
A value that is absent is absent
Section titled “A value that is absent is absent”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: / 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.
What rx-overflow on the NAS port is
Section titled “What rx-overflow on the NAS port is”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-overflowevents 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-bytesminusdriver-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,ether3and the CPU path show nothing. - The frames were large: the 1024-and-up frame-size bucket on
ether1has 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_squeezecorrelates 0.04 and theswitch0interrupts 0.05 against the overflows, with the agent’s 10 Hz data binned to 10 s. No pause frames onether1in 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.
What the tier costs the router
Section titled “What the tier costs the router”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_. 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 % |
Scroll sideways to see every column
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.
The conntrack count
Section titled “The conntrack count”--conntrack-every 10s asks / 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.