Skip to content

Mesh Issues Analysis ​

Mesh Issues Analysis is a scheduled health report that reads data your sources have already collected β€” node telemetry, traceroutes, NeighborInfo, and (where enabled) packet-log receptions β€” and surfaces the routing and RF problems that data already shows. It does not probe the mesh to find them.

The passive guarantee ​

This feature sends zero packets. It never generates a traceroute, never polls a node, and never sends a message. Every finding comes from packets your node or your MQTT sources already received and stored. "Run analysis now" runs the same read β€” it is passive too.

This matters because LoRa is a shared, half-duplex medium: a feature that looks cheap on a desk with one node becomes mesh-wide congestion on a busy channel with 200. A health report earns the right to run on a schedule (24 hours by default) only by staying entirely off the air. See the Mesh Impact checklist for the reasoning this feature was built against.

How it works ​

Analysis is a global, cross-source batch job, structurally identical to Position Estimation: it runs on a schedule (not in realtime), pools data from every source you can read, and stores findings in one global table. MeshCore sources take part in a short list of checks β€” see MeshCore repeaters. The other rules read Meshtastic roles, airtime and key flags that MeshCore nodes do not have, so they never see a MeshCore node.

Findings are grouped into three tiers, run in order:

  • Tier A β€” Node health. Reads nodes and telemetry directly: battery, uptime, airtime/channel utilization, and the position history of infrastructure-role nodes.
  • Tier B β€” RF adjacency graph. Builds a graph of which nodes can hear each other from traceroutes, NeighborInfo, and (where enabled) the MQTT/Meshtastic packet logs, then evaluates router clustering, redundancy, link asymmetry, and coverage over that graph.
  • Tier C β€” Node flags. Folds in flags other MeshMonitor services already compute (packet-rate, key-security, and clock-offset detectors) and adds one new check of its own: nodes broadcasting position or telemetry far more often than the mesh needs.

Each finding tracks its own lifecycle: new, reopened (closed, then seen again), updated (still open, evidence refreshed), or closed (stopped recurring). A finding is never deleted outright β€” see Dismissing and auto-close.

What each rule means ​

[official] marks a threshold sourced from Meshtastic's own ROUTER_LATE guidance. [MeshMonitor] marks MeshMonitor's own judgement β€” reasonable, but not an official number, and usually tunable (see Settings).

For a detailed explanation of every test ID β€” what it checks, why it matters, and what to do about it β€” see the Test Reference.

Tier A β€” Node health ​

RuleFires whenThresholdSeverity
A1 Deprecated roleNode's role is REPEATER or ROUTER_CLIENTβ€”warning
A2a Chatty nodeMean airUtilTx over 24 h exceeds the ceiling, β‰₯6 samples8% [official], tunablewarning
A2b Congested areaβ‰₯3 nodes in one ~5.5 km bin, mean channelUtilization over the ceiling25% [official], tunablewarning
A2b Congested node1–2 nodes in a bin exceed the ceiling β€” not enough neighbors to confirm area-wide congestion25% [official], tunableinfo
A3 Infra node on failing powerInfrastructure role, not powered (battery β‰ 101), and either β‰₯2 uptime resets in 7 days or battery <20%20% battery floor [MeshMonitor]warning (resets) / info (battery only)
A4 Mobile infra nodeInfrastructure role whose observed position span exceeds the mobility distance500 m [MeshMonitor], tunablewarning
A5 Cosplay routerROUTER role with isUnmessagable=false (firmware β‰₯2.5.0), or broadcasting telemetry with a median interval under 2 hours (β‰₯5 samples, needs the packet log)2 h median, 5 samples [MeshMonitor]info
A6 Stale nodeInfrastructure node not heard for longer than its stale window and for no more than 30 days. At least one source that hears it must itself have heard something inside the window24 h Meshtastic, 96 h MeshCore [MeshMonitor], tunablewarning
A7 No positionInfrastructure node with no reported position and no position override (clauses: no_position, position_sharing_disabled on Meshtastic with 0 precision bits, advert_without_position on MeshCore when the last advert carried none)β€”info
A8 Low batteryInfrastructure node whose latest in-window battery reading is under the line. A3 keeps a node whose own battery clause fires, so a node shows once20 % Meshtastic, 3300 mV MeshCore [MeshMonitor], tunablewarning
A9 Bad positionInfrastructure node whose reported position is Null Island or out of range (overrides ignored). Evidence names the problem, never the coordinatesβ€”info

A6–A9 run on both protocols from one implementation and look at infrastructure nodes only (Meshtastic routing roles, MeshCore repeaters and room servers). Stale is measured from lastHeard, the local node is never flagged, and a node unheard for over 30 days counts as retired and is left alone (its finding auto-closes). The MeshCore window is longer because a far repeater only refreshes lastHeard on a flood advert (47 h by default). A8 reads telemetry samples, not the last value on the node row, so an old reading cannot raise it; a Meshtastic node reporting 101 % (powered) or 0 % without a voltage is skipped. A9 mostly stays quiet while Discard invalid positions is on, because Null Island fixes are then never stored; it reports legacy rows and installs that turned discard off. None of these rules send anything on the radio or notify.

Tier B β€” RF adjacency graph ​

RuleFires whenThresholdSeverity
B1 Router clusterβ‰₯2 ROUTER/REPEATER-role nodes are mutually audible and their non-cluster client neighbors overlap significantly (β‰₯90%)cluster size 2, 90% client overlap [MeshMonitor]warning (β‰₯2), critical (β‰₯4), info (inferred-only evidence)
B2 Redundant routerOne infra node's direct-neighbor set is β‰₯90% covered by another's, both with β‰₯3 neighbors90% overlap [MeshMonitor]warning
B3 Asymmetric linkDirectional mean SNR between two nodes differs by more than the ceiling, β‰₯3 samples per direction6 dB [MeshMonitor], tunablewarning (infra endpoint involved) / info
B4 Idle routerAn infra node is heard direct but carries <1% of its area's traceroute hops while a peer carries >10%1%/10% split [MeshMonitor]info always
B5 Load-bearing CLIENTA non-infra node appears as an intermediate hop in β‰₯10 traceroutes and β‰₯25% of its area's paths25% area share [MeshMonitor]warning (not fixed+powered) / info
B6 Hop horizonOver 50% of a node's deduped observed packets arrive with hopLimit=0, β‰₯20 packets50%/20 packets [MeshMonitor]info
B7 Coverage shadowAn MQTT-only node (never RF-heard) falls inside a nearby router's observed RF rangeβ€” (info only)info, default rule toggle

Tier C β€” Node flags ​

RuleFires whenThresholdSeverity
C1 Excessive packet rateisExcessivePackets flag is set (own detector, deduped across vantages)β€”warning
C1 Key security issueAny of low-entropy key, duplicate key, or key mismatch is flaggedβ€”warning
C1 Clock offsetisTimeOffsetIssue flag is set30 min, fixed by TIME_OFFSET_THRESHOLD_MINUTES (env)info
C2 Over-broadcastingA non-tracker node's deduped position or telemetry median inter-arrival falls under the floor, β‰₯6 samples300 s [MeshMonitor], tunablewarning (unpowered or under half the floor) / info

C2 exempts TRACKER, SENSOR, and TAK_TRACKER roles β€” those roles exist to report frequently by design, and the rule would otherwise fire on every correctly configured device of that type. A node with no known role is skipped rather than assumed innocent or guilty.

Where the thresholds come from ​

Only two numbers here are official Meshtastic guidance, both from the firmware's ROUTER_LATE documentation:

  • ChUtil (channel utilization) above 25% means an RF area is congested.
  • AirUtilTX above 7–8% means a single node is using too much of the channel. MeshMonitor uses 8% as the default ceiling.

Every other number β€” cluster sizes, overlap ratios, hop-share splits, sample minimums, window lengths β€” is MeshMonitor's own judgement, chosen to avoid false positives on a small or sparse mesh rather than derived from an official source. Five of them are exposed as tunable settings; the rest stay fixed in code, listed here for reference:

ConstantValueRule(s)
Chatty-node minimum samples6A2a
Utilization window24 hA2a, A2b
Congested-area minimum nodes3A2b
Geographic bin size~0.05Β° (~5.5 km)A2b
Battery-low floor20%A3
Battery minimum samples3A3
Uptime-reset minimum count2A3
Power window168 h (7 days)A3, A4
Minimum position precision17 bits (~305 m)A4
Unmessagable-aware firmware floor2.5.0A5
Telemetry-cadence median ceiling2 hA5
Telemetry-cadence minimum samples5A5
Directional-SNR minimum samples3 per directionB3
Gateway-direct minimum receptions3evidence class 3
Gateway cell size cap64 nodesevidence class 3
Gateway SNR sample cap25 per edgeevidence class 3
Redundant-router minimum neighbors3B2
Redundant-router overlap ratio90%B2
Idle-router minimum area paths20B4
Idle-router max hop share1%B4
Idle-router peer min hop share10%B4
Load-bearing minimum traceroutes10B5
Load-bearing minimum area share25%B5
Hop-horizon exhausted ratio50%B6
Hop-horizon minimum packets20B6
Coverage-shadow minimum range samples3B7
Coverage-shadow range cap25 kmB7
Router-cluster warning size2B1
Router-cluster critical size4B1
Router-cluster client-overlap ratio90%B1 (shared with B2)
Router-cluster minimum non-cluster neighbours3B1 (shared with B2)
Over-broadcast minimum samples6C2
Auto-close clean-run count3all rules
Evidence list cap25 entriesall list-shaped evidence

Reading the report ​

The report opens with summary tiles β€” one per issue type present, showing the count, worst severity, and how many findings are new or reopened since the last run. Click a tile to filter to that type. A filter bar (severity, tier, type, source, node-name search) and a "Show dismissed" toggle apply to everything below.

Two views share those filters:

  • By issue β€” one collapsible section per issue type, each a compact, sortable table with columns chosen for that rule (SNR per direction for asymmetric links, resets and battery floor for power findings, and so on). Sections stay collapsed until you expand them β€” nothing loads until you ask β€” except a section holding a critical finding, which opens itself. Click a row for the full evidence.
  • By node β€” findings grouped by node, worst first, with one badge per issue type. A node with four findings is one row here; this is the view to work from when deciding which node to fix. Findings not tied to a single node (clusters, links, congested areas) sit in a pinned "Mesh-wide" group at the top.

Each type section and node row carries a bulk-actions menu: dismiss or restore everything in that group at once, or mute the rule outright (see Muting a rule). Bulk actions only ever touch findings your account can see.

MeshCore repeaters ​

MeshCore contacts are pooled across every enabled meshcore and meshcore_mqtt source by public key. A node counts as infrastructure when its advert type is Repeater or Room Server. Several checks run on them, all from data MeshMonitor already holds (no packets are sent):

RuleFires whenNotes
A4 Mobile infrastructureA repeater or room server's position span (from meshcore_position_history) is over the mobile-span thresholdSame threshold and wording as Meshtastic A4
A3 Infra power (uptime resets only)Uptime in the repeater's status telemetry drops two or more times in the windowThere is no battery percentage or voltage clause for MeshCore
C2 Advert cadenceA repeater's median flood advert gap is under 6 hours, or its median zero-hop advert gap is under 60 minutes (at least 4 adverts)Needs the MeshCore packet log (meshcore_packet_log_enabled); copies of one advert heard by several observers count once

When the MeshCore packet log is off, the advert check shows in the coverage preface as skipped instead of reporting a clean result.

A6 to A9 (stale, no position, low battery, bad position) also run on repeaters and room servers; see the Tier A table. MeshCore battery comes from the status telemetry the operator already polls: mc_battery_mv (a local companion) and mc_status_battery_volts (a remote repeater, scaled to mV). A repeater with no battery telemetry in the window shows as skipped for A8, not as healthy. A repeater that is the connected source itself is judged only when its node row exists; the poller does not create one.

A MeshCore finding is keyed mc:<public key> and has no node number. Its evidence carries the public key, the contact name and the contributing source ids. Clicking the name opens that contact on its source (a picker appears if the finding spans several sources). If you cannot read every source that contributed, the contact name is hidden in the evidence. In the By Node view, MeshCore findings fall under the Mesh-wide group because that view groups by node number.

Clickable node references ​

Every place a node's name shows up in a finding, the By Node group headers, the member and "shared with" chips inside evidence, is a clickable link. Click one to jump straight to a DM thread with that node on its source's Messages tab. When the same node exists on more than one source, a small picker opens so you can choose which source to open the thread on. Cell contents that used to overflow the table into a horizontal scrollbar (the C1 Details column and the multi-source list, in particular) now wrap within their cell instead.

Acting on findings ​

The recommendations in this report never suggest ROUTER, and never say "promote". Meshtastic's own guidance is that ROUTER is a legacy role kept for compatibility, not a fix to reach for. Depending on the finding, the right move is usually one of:

  • CLIENT β€” the default for a mobile or non-infrastructure node. If a node shows up in a load-bearing or redundant-router finding but isn't fixed and powered, deploying another CLIENT nearby (not upgrading this one) is the fix.
  • CLIENT_BASE β€” for a node that is fixed and powered but not meant to route for others; the natural landing spot for a mobile-flagged infrastructure node (A4) or an over-committed CLIENT (B5) once it's confirmed stationary.
  • ROUTER_LATE β€” the modern replacement for ROUTER/ROUTER_CLIENT/REPEATER when a location genuinely needs a dedicated router. It routes with lower priority so it doesn't dominate a busy channel.

Some findings point to hardware or siting, not a role change at all β€” B3 (asymmetric link) usually means an antenna, feedline, or siting difference, and A3 (infra node on failing power) means checking the power budget.

Coverage and why a rule is silent ​

Not every rule can evaluate on every install. A coverage preface at the top of the report states, in plain language, what evidence a run actually had and which rules went quiet for lack of it:

  • B3, B6, and B7 lean on optional data. B3 (asymmetric link) needs traceroutes or MQTT-derived SNR samples; B6 (hop horizon) needs a packet log (Meshtastic or MQTT) enabled; B7's evidence gets stronger with an MQTT source enabled, though it can still fire without one. The preface tells you which of these are off, as a hint β€” never as an instruction to turn something on. Both packet logs are opt-in, per-source features with their own storage cost, and this report doesn't get to make that call for you.
  • B7 estimates range, it doesn't model terrain. A router's "observed range" is the farthest positioned direct neighbor it has actually heard, capped at 25 km. It has no idea about hills or buildings between two points β€” a hill on one bearing doesn't shrink the estimate on a clear bearing 90Β° away.
  • B6's hop delta is a lower bound. Firmware 2.7+ gives favourite-router hops zero cost, so a packet can travel further than its hopLimit decrements suggest. B6 can under-flag as a result β€” never over-flag.
  • C2's candidate gate is a mean, not the median it reports. Finding candidates cheaply for a mesh-wide scan requires a fast first pass; the final number shown is always a true median, but the initial gate uses a mean, which can miss a node that broadcasts in a tight burst and then goes silent for days. This is deliberately the safe direction to be wrong in.
  • A5's telemetry-cadence clause needs the packet log. The second "cosplay router" signal β€” a ROUTER broadcasting telemetry far more often than the 12-hour role default (median under 2 hours, at least 5 samples) β€” reads broadcast TELEMETRY_APP receptions from the packet log, which is opt-in and off by default. With the packet log disabled the clause reports itself as unavailable and A5 falls back to the isUnmessagable clause alone; it never fires on missing data.

Dismissing and auto-close ​

Dismissing a finding hides it for every user β€” it's administrative state, not a per-viewer preference β€” and is reversible at any time by restoring it. Dismissing does not stop a finding's lifecycle: if the underlying condition is still there on the next run, a dismissed finding still gets its evidence refreshed; only its visibility changes.

A finding that stops recurring auto-closes after a number of consecutive clean runs β€” the "Auto-close after" setting, default 3 (about 3 days at the default 24-hour cadence), clamped to 1–20. Disabling a tier or a rule doesn't delete its existing findings; they simply stop reappearing and auto-close the same honest way. If you set the analysis frequency much faster than 24 hours, the auto-close window shrinks along with it (a 1-hour cadence auto-closes in about 3 hours); much slower, and it lengthens correspondingly.

Muting a rule ​

Each rule (all 22 issue types, one row per rule, grouped by tier) has its own mute checkbox under Global Settings β†’ Mesh Issues Analysis β†’ Individual rules, alongside the coarser Tier A/B/C toggles. Uncheck one to stop detecting it without touching anything else.

Muting is not deleting. A muted rule contributes no new findings starting with the next run, but it does not delete or hide the findings it already raised β€” the report still lists them, with everything (dismiss, restore, evidence) working normally. They stop being re-detected, accumulate clean runs the same way a finding does when its underlying condition genuinely goes away, and auto-close after the "Auto-close after" run count (default 3 β€” see Dismissing and auto-close above). There is no separate "close muted findings now" action: the honest signal that a mute took effect is the same clean-run counter every other rule already uses.

Saving the mute list only writes settings β€” like every other control on this page, it never triggers a run and never resets the schedule's last-run timer.

If you decide a rule was worth keeping after all, re-check it. The next run picks it back up immediately; nothing needs to be restored, since findings that hadn't yet auto-closed were never touched.

Settings ​

Open Global Settings (the gear icon in the dashboard sidebar) β†’ Mesh Issues Analysis. The controls are global and require settings:write.

Schedule

SettingDefaultNotes
EnabledonTurns the whole scheduled job on or off.
Analysis frequency24 hoursHow often the batch job runs.
Lookback window168 hours (7 days)How far back telemetry/traceroute data is pooled.
Traceroute pair bucket6 hoursHow the traceroute corpus dedupes repeated observations of the same node pair before sampling.

Rules

SettingDefaultNotes
Tier AonNode health rules (A1–A9).
Tier BonRF adjacency graph rules (B1–B7).
Tier ConNode-flag rules (C1, C2).
Individual rulesall onBelow the tier toggles, one checkbox per rule (grouped by tier) mutes just that rule β€” see Muting a rule. Coverage shadow (B7) lives here now; it's the one rule observed firing at pathological volume (500+ findings) on a busy mesh.

Thresholds

SettingDefaultRangeProvenance
Airtime TX ceiling8%1–50%[official]
Channel utilization ceiling25%5–100%[official]
Mobile span500 m50–50,000 m[MeshMonitor]
Link SNR asymmetry6 dB1–30 dB[MeshMonitor]
Broadcast interval floor300 s30–3,600 s[MeshMonitor]
Router cluster max link30 km1–500 km[MeshMonitor], B1 distance guard
Auto-close after3 clean runs1–20[MeshMonitor]
Stale after, Meshtastic24 h2–720 h[MeshMonitor], A6
Stale after, MeshCore96 h12–720 h[MeshMonitor], A6
Low battery, Meshtastic20 %1–80 %[MeshMonitor], A8 and the A3 battery clause
Low battery, MeshCore3300 mV2500–4100 mV[MeshMonitor], A8. LiFePO4 packs rest near 3.2–3.3 V, so lower this for them

Every value is clamped when read, not rejected on save: an out-of-range number you enter is silently pulled back to the nearer bound the next time the scheduler (or this settings page) reads it, so a bad value degrades to a bounded run instead of an unbounded one. Saving these settings never runs analysis and never resets the schedule's last-run timer β€” use Run analysis now for that.

A Run analysis now button triggers an immediate, still-passive run. If one is already in progress, a second click is rejected rather than queued. The last-run line below the controls shows when analysis last completed and how many findings it produced; after a container restart, this line is recovered from the last successful run's stored summary rather than going blank until the next scheduled run.

Overlap with the Security tab ​

C1's three findings β€” excessive packet rate, key security, and clock offset β€” surface the exact same node flags the Security tab already shows. That's intentional, not a bug: C1 doesn't add new detection, it puts those flags in the same health report as the routing and RF findings, with dismiss/restore and the same auto-close lifecycle every other finding gets. If you see a node flagged both places, they're the same underlying signal β€” dismissing it here doesn't clear it there, and vice versa, since the two features track state independently.

Permissions & API ​

Findings and status map to permitted source read access; the routes gate the same way resolvePermittedSourceIds does elsewhere in the app β€” a caller who can't read any source gets an empty/forbidden response rather than a 500. Dismissing and restoring, and the scheduled job's manual trigger, require the global settings:write permission, with a visibility check that returns 404 (not 403) for a finding whose contributing sources you can't read β€” so a settings:write holder with narrow source access can't dismiss, or even discover the existence of, a finding outside their permitted sources.

  • GET /api/analysis/mesh-issues β€” list findings for your permitted sources
  • GET /api/analysis/mesh-issues/status β€” scheduler status, resolved thresholds, and the last run's coverage summary
  • POST /api/analysis/mesh-issues/run-now β€” trigger a run immediately (settings:write)
  • POST /api/analysis/mesh-issues/:id/dismiss / /restore β€” hide/unhide a finding for everyone (settings:write, plus the visibility check above)
  • Settings are saved through POST /api/settings (settings:write)