# mikroscope

Sub-second kernel telemetry from inside a MikroTik router, with the cost of the observer measured rather than claimed.

Source: https://jmrplens.github.io/mikroscope/

## Measured, not budgeted

- [**2.85 %** — of one core at 10 Hz, the install default](/mikroscope/cost/)
- [**31.3 MiB** — resident memory at 10 Hz](/mikroscope/cost/)
- [**17.81 %** — of one core at 100 Hz, the CLI's cap](/mikroscope/cost/rate-ceiling/)
- [**0 / 0** — gaps and drops, in every sink, across five runs](/mikroscope/cost/rate-ceiling/)

The first three from the agent's own cgroup and `/metrics`, the fourth from the three sinks the collector forwarded to, on an RB5009 (4 × 1.4 GHz Cortex-A72, RouterOS 7.24.2), 60 s windows at steady state, 2026-09-15. The 10 Hz figures are above the ≤ 2 % and ≤ 16 MiB budget.

## A one-second average is a report about a second

The RouterOS API reports `cpu-load` once a second. A core saturated for 100 ms and idle for the other 900 moves a four-core, one-second average by 2.5 %. That is arithmetic, not a measurement, and the figure is true: it just cannot say when.

The agent reads `/proc/stat`, `/proc/interrupts`, `/proc/softirqs` and `/proc/net/softnet_stat` from inside the router, at 10 Hz by default, and ships raw tick deltas with the interval each one covers. It never computes a percentage; the window is yours.

The floor is the kernel's, not the tool's. `/proc/stat` counts in ticks of 10 ms, so a 100 ms sample resolves one core to 10 % steps. On the RB5009 (RouterOS 7.24.2, Linux 5.6.3) there is no PSI and no schedstat to go finer: both files are absent, checked 2026-09-11.

## What it costs, at three rates

Each row is one 60 s window with the ring already full. Memory differs by row because the ring and the memory limit do.
Measured on RB5009UG+S+ · 4 × 1.4 GHz Cortex-A72 · RouterOS 7.24.2 · 2026-09-15 · 60 s windows at steady state (ring full), full source set, collector forwarding to a file, a Prometheus exposition and InfluxDB 3 at once

The measured runs:

| rate | floors | CPU of one core | µs/sample | RSS | slipped ticks | gaps / drops |
| --- | --- | --- | --- | --- | --- | --- |
| 10 Hz (default) | default | **2.85 %** | 2 856 | 31.3 MiB | **0** | 0 / 0 |
| 50 Hz | default | **10.13 %** | 2 026 | 51.9 MiB | **0** | 0 / 0 |
| 100 Hz | default | **17.81 %** | 1 781 | 76.5 MiB | 5 (0.08 %) | 0 / 0 |

Nothing was lost at any of these rates: every sink reported 0 gaps and 0 drops, and the delivered rate matched the configured one to three figures. At the default floors and 100 Hz, a whole tick's sources were read in under 2 ms for 97.5 % of samples, inside a 10 ms period.

[All five runs, including every source on every tick →](/mikroscope/cost/rate-ceiling/)

## The router's CPU from the kernel, its interfaces from the API

### Kernel tier · the agent · 10 to 100 Hz

Global inside the container, so these are the router's own: per-core CPU ticks, interrupts, softirqs, softnet drops and time squeezes, `/proc/meminfo`, `/proc/vmstat`, load and disk I/O. A privileged container adds the kernel log as timestamped events and the global slab caches.

### API tier · the collector · 1 Hz

The container has its own network namespace, so `/proc/net/dev` describes the container, not the router. Interface bytes and packets come from the RouterOS API and are merged by the collector, not interpolated. `privileged=yes` does not change that (checked 2026-09-12).

## Every write listed before it is made

Download the archive for your platform from the release, or build the CLI from a checkout with `make build`. The router needs RouterOS 7.24 or later — the container step writes `privileged=`, an attribute earlier 7.x releases reject — with the `container` package and `device-mode container=yes`, which MikroTik gates behind a reset-button press or a power cycle. arm64, arm and x86_64; not MIPS, not TILE.

1. `mikroscope doctor`

   Read-only preflight; names the fix for anything missing.

2. `mikroscope plan`

   Every RouterOS command, nothing written.

3. `mikroscope install`

   Doctor, confirmation, the writes, then a probe of the agent. The image comes from your own Go toolchain, from the published agent tar, or from the registry the router pulls it from.

4. `mikroscope status`

   Ownership counts and the agent's health.

5. `mikroscope uninstall`

   Removes and verifies.

**What `install` writes to your router**

- a veth
- one address
- one interface-list membership
- one address-list entry
- an envlist
- the image tar, unless `--remote-image` has the router pull the image
- the container

Every object carries the comment `mikroscope:<name> (managed by mikroscope)`

`mikroscope plan` prints every command before anything is written.

`uninstall` removes by exact tag plus identity, never by pattern, and fails naming the step if anything remains.

## What is not claimed

> **Not measured, so not claimed**
>
> Any rate on a board that is not this RB5009, and any traffic load heavier than this router's ordinary evening, about 30 Mbit/s. Cost scales with core speed, source set and ring size: measure it on your own device before you budget for it. The project has run on one device, an RB5009UG+S+ on RouterOS 7.24.2, arm64; the arm and x86_64 builds are cross-built and checked in CI and have never run on hardware, and seven of the ten sinks are exercised only against fakes.

## Where to go next

- [What it is](/mikroscope/start/): Two programs, one container, four limits stated first
- [Five minutes with a router](/mikroscope/start/walkthrough/): Install, record while you change something, draw the chart, from a real RB5009 recording
- [Installing the agent](/mikroscope/install/): Four ways to get the agent onto the router, what install writes and in which order, and how uninstall removes only what it created
- [The collector](/mikroscope/sinks/): Pull, merge, derive, fan out to ten sinks, and why a slow one never stops the loop
- [How to read what it shows](/mikroscope/playbooks/): A production fault the API could not see, provoked faults, and the idle shape they are read against
- [What the numbers do not say](/mikroscope/cost/limits/): Every limit on the figures above
