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 are excluded β meshcore_nodes has no role/airUtilTx equivalent for the rules below to evaluate.
Findings are grouped into three tiers, run in order:
- Tier A β Node health. Reads
nodesandtelemetrydirectly: 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 β
| Rule | Fires when | Threshold | Severity |
|---|---|---|---|
| A1 Deprecated role | Node's role is REPEATER or ROUTER_CLIENT | β | warning |
| A2a Chatty node | Mean airUtilTx over 24 h exceeds the ceiling, β₯6 samples | 8% [official], tunable | warning |
| A2b Congested area | β₯3 nodes in one ~5.5 km bin, mean channelUtilization over the ceiling | 25% [official], tunable | warning |
| A2b Congested node | 1β2 nodes in a bin exceed the ceiling β not enough neighbors to confirm area-wide congestion | 25% [official], tunable | info |
| A3 Infra node on failing power | Infrastructure 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 node | Infrastructure role whose observed position span exceeds the mobility distance | 500 m [MeshMonitor], tunable | warning |
| A5 Cosplay router | ROUTER 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 |
Tier B β RF adjacency graph β
| Rule | Fires when | Threshold | Severity |
|---|---|---|---|
| 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 router | One infra node's direct-neighbor set is β₯90% covered by another's, both with β₯3 neighbors | 90% overlap [MeshMonitor] | warning |
| B3 Asymmetric link | Directional mean SNR between two nodes differs by more than the ceiling, β₯3 samples per direction | 6 dB [MeshMonitor], tunable | warning (infra endpoint involved) / info |
| B4 Idle router | An 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 CLIENT | A non-infra node appears as an intermediate hop in β₯10 traceroutes and β₯25% of its area's paths | 25% area share [MeshMonitor] | warning (not fixed+powered) / info |
| B6 Hop horizon | Over 50% of a node's deduped observed packets arrive with hopLimit=0, β₯20 packets | 50%/20 packets [MeshMonitor] | info |
| B7 Coverage shadow | An 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 β
| Rule | Fires when | Threshold | Severity |
|---|---|---|---|
| C1 Excessive packet rate | isExcessivePackets flag is set (own detector, deduped across vantages) | β | warning |
| C1 Key security issue | Any of low-entropy key, duplicate key, or key mismatch is flagged | β | warning |
| C1 Clock offset | isTimeOffsetIssue flag is set | 30 min, fixed by TIME_OFFSET_THRESHOLD_MINUTES (env) | info |
| C2 Over-broadcasting | A non-tracker node's deduped position or telemetry median inter-arrival falls under the floor, β₯6 samples | 300 s [MeshMonitor], tunable | warning (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:
| Constant | Value | Rule(s) |
|---|---|---|
| Chatty-node minimum samples | 6 | A2a |
| Utilization window | 24 h | A2a, A2b |
| Congested-area minimum nodes | 3 | A2b |
| Geographic bin size | ~0.05Β° (~5.5 km) | A2b |
| Battery-low floor | 20% | A3 |
| Battery minimum samples | 3 | A3 |
| Uptime-reset minimum count | 2 | A3 |
| Power window | 168 h (7 days) | A3, A4 |
| Minimum position precision | 17 bits (~305 m) | A4 |
| Unmessagable-aware firmware floor | 2.5.0 | A5 |
| Telemetry-cadence median ceiling | 2 h | A5 |
| Telemetry-cadence minimum samples | 5 | A5 |
| Directional-SNR minimum samples | 3 per direction | B3 |
| Gateway-direct minimum receptions | 3 | evidence class 3 |
| Gateway cell size cap | 64 nodes | evidence class 3 |
| Gateway SNR sample cap | 25 per edge | evidence class 3 |
| Redundant-router minimum neighbors | 3 | B2 |
| Redundant-router overlap ratio | 90% | B2 |
| Idle-router minimum area paths | 20 | B4 |
| Idle-router max hop share | 1% | B4 |
| Idle-router peer min hop share | 10% | B4 |
| Load-bearing minimum traceroutes | 10 | B5 |
| Load-bearing minimum area share | 25% | B5 |
| Hop-horizon exhausted ratio | 50% | B6 |
| Hop-horizon minimum packets | 20 | B6 |
| Coverage-shadow minimum range samples | 3 | B7 |
| Coverage-shadow range cap | 25 km | B7 |
| Router-cluster warning size | 2 | B1 |
| Router-cluster critical size | 4 | B1 |
| Router-cluster client-overlap ratio | 90% | B1 (shared with B2) |
| Router-cluster minimum non-cluster neighbours | 3 | B1 (shared with B2) |
| Over-broadcast minimum samples | 6 | C2 |
| Auto-close clean-run count | 3 | all rules |
| Evidence list cap | 25 entries | all 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.
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
hopLimitdecrements 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_APPreceptions 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 theisUnmessagableclause 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 18 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
| Setting | Default | Notes |
|---|---|---|
| Enabled | on | Turns the whole scheduled job on or off. |
| Analysis frequency | 24 hours | How often the batch job runs. |
| Lookback window | 168 hours (7 days) | How far back telemetry/traceroute data is pooled. |
| Traceroute pair bucket | 6 hours | How the traceroute corpus dedupes repeated observations of the same node pair before sampling. |
Rules
| Setting | Default | Notes |
|---|---|---|
| Tier A | on | Node health rules (A1βA5). |
| Tier B | on | RF adjacency graph rules (B1βB7). |
| Tier C | on | Node-flag rules (C1, C2). |
| Individual rules | all on | Below 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
| Setting | Default | Range | Provenance |
|---|---|---|---|
| Airtime TX ceiling | 8% | 1β50% | [official] |
| Channel utilization ceiling | 25% | 5β100% | [official] |
| Mobile span | 500 m | 50β50,000 m | [MeshMonitor] |
| Link SNR asymmetry | 6 dB | 1β30 dB | [MeshMonitor] |
| Broadcast interval floor | 300 s | 30β3,600 s | [MeshMonitor] |
| Router cluster max link | 30 km | 1β500 km | [MeshMonitor], B1 distance guard |
| Auto-close after | 3 clean runs | 1β20 | [MeshMonitor] |
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 sourcesGET /api/analysis/mesh-issues/statusβ scheduler status, resolved thresholds, and the last run's coverage summaryPOST /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)
Related β
- Analysis & Reports β the cross-source reports workspace this feature's report card lives in
- Position Estimation β the other global, scheduled, cross-source batch job this feature is modeled on
- Global Settings
- Packet Monitor β the opt-in packet logs that strengthen B3/B6/B7's evidence
- Multi-Source