Does repeater time matter in MeshCore?

  /   7 minutes   /   tech   meshcore   lora  

TL;DR: No, not for actual mesh packet repeating. A repeater with the wrong time will flood and forward packets identically to one with the correct time. The only effects are cosmetic/informational.

Edit: This post was challenged with five counterarguments. I verified each one against the source. Three were wrong, one was already covered, and one raised a valid operational point that I've added. The core thesis is unchanged. See section 10 for the full breakdown.


I keep seeing this claim pop up: "If your repeater's time is wrong, it'll mess up the mesh." It gets repeated (pun intended) in forums and chat rooms often enough that people accept it as fact. I already knew this wasn't true from my own research building DongLoRa, but I wanted to go straight to the source and gather the facts to put this to rest once and for all.

So I did what any reasonable person would do in 2026. I pointed Claude at the MeshCore source code and asked it to trace every code path involved in packet repeating, and tell me exactly where the RTC clock is and isn't consulted.

Yes, people will call this "vibe coded slop." They can call it whatever they want. But unless they show up with actual facts and data that disprove any of what follows -- which I genuinely welcome, I have zero problem being wrong -- then I don't know what to tell them. Welcome to 2026. This is how we work now. An LLM read the source, I verified it, and here's the report. If you don't like the process, attack the substance.


1. Flood repeat -- no time involved

The flood repeat path is in src/Mesh.cpp:328-341:

DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
  uint8_t n = packet->getPathHashCount();
  if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit()
    && (n + 1)*packet->getPathHashSize() <= MAX_PATH_SIZE && allowPacketForward(packet)) {
    ...
    return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d);
  }
  return ACTION_RELEASE;
}

The checks are:

  • Is it a flood packet? (header bits)
  • Is it marked do-not-retransmit? (header == 0xFF)
  • Is there room for one more hop in the path? (hop count vs MAX_PATH_SIZE)
  • Does allowPacketForward() permit it?

Wall-clock time: not consulted.

The repeater's override of allowPacketForward() is at examples/simple_repeater/MyMesh.cpp:416-438:

bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
  if (_prefs.disable_fwd) return false;
  if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
  if (packet->isRouteFlood() && recv_pkt_region == NULL) { ... return false; }
  if (packet->isRouteFlood() && _prefs.loop_detect != LOOP_DETECT_OFF) {
    if (isLooped(packet, maximums)) { return false; }
  }
  return true;
}

Checks: forwarding disabled pref, flood_max hop limit, region/transport code match, loop detection (counting how many times this node's hash appears in the path). Zero time references.


2. Direct forwarding -- no time involved

The direct forward path is at src/Mesh.cpp:75-104:

if (pkt->isRouteDirect() && pkt->getPathHashCount() > 0) {
    if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) {
      if (!_tables->hasSeen(pkt)) {
        removeSelfFromPath(pkt);
        uint32_t d = getDirectRetransmitDelay(pkt);
        return ACTION_RETRANSMIT_DELAYED(0, d);
      }
    }
    return ACTION_RELEASE;
}

Checks: Is it direct-routed? Is this node the next hop? allowPacketForward()? Deduplication? Zero time references.


3. Deduplication -- no time involved

src/Packet.cpp:41-50 shows the packet hash is SHA256(payload_type || payload):

void Packet::calculatePacketHash(uint8_t* hash) const {
  SHA256 sha;
  uint8_t t = getPayloadType();
  sha.update(&t, 1);
  ...
  sha.update(payload, payload_len);
  sha.finalize(hash, MAX_HASH_SIZE);
}

The hash is computed from the packet's own payload, not the repeater's clock. For an advert being forwarded, the payload contains the original sender's timestamp, not the repeater's. The deduplication table (SimpleMeshTables.h:43-81) is a simple circular buffer with no time-based aging -- entries are evicted only when the buffer wraps. Zero involvement of the repeater's clock.


4. Retransmit delay -- relative only

examples/simple_repeater/MyMesh.cpp:526-533:

uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
  uint32_t t = (_radio->getEstAirtimeFor(...) * _prefs.tx_delay_factor);
  return getRNG()->nextInt(0, 5*t + 1);
}
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
  uint32_t t = (_radio->getEstAirtimeFor(...) * _prefs.direct_tx_delay_factor);
  return getRNG()->nextInt(0, 5*t + 1);
}

Based on estimated airtime and random jitter. Uses millis() for scheduling (relative timing), not the RTC. Zero wall-clock time involvement.


5. Rx delay -- relative only

examples/simple_repeater/MyMesh.cpp:521-524:

int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
  if (_prefs.rx_delay_base <= 0.0f) return 0;
  return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
}

Based on signal quality score and airtime. Zero wall-clock time.


6. filterRecvFloodPacket() -- no time involved

examples/simple_repeater/MyMesh.cpp:535-546:

bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
  if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
    recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
  } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) {
    ...
  }

Only determines the region for transport code matching. Zero time references.


7. Advert forwarding -- sender timestamp preserved

When a repeater receives another node's advert, the processing at src/Mesh.cpp:239-277 extracts the timestamp from the payload (the original sender's time), verifies the sender's signature against it, calls onAdvertRecv(), then calls routeRecvPacket() -- the standard flood repeat path analyzed above.

The advert payload -- including the original sender's embedded timestamp -- is re-broadcast byte-for-byte. The repeater's own clock is never consulted during this process.


8. What the clock DOES affect -- not forwarding

Here's the complete list of things that actually use the RTC clock. None of them affect routing or forwarding, but some have real operational consequences.

A. The repeater's own advert (src/Mesh.cpp:404-405):

uint32_t emitted_timestamp = _rtc->getCurrentTime();
memcpy(&packet->payload[len], &emitted_timestamp, 4);

When the repeater generates its own advertisement, it embeds getCurrentTime() as the timestamp. This is part of the Ed25519 signature. If the clock is wrong, this repeater's advert will carry a wrong timestamp. This is what MeshMapper, web maps, and companion apps display as "last seen" for this specific repeater. Cosmetic.

B. The neighbor table display (examples/simple_repeater/MyMesh.cpp:85, 358, 1045):

neighbour->heard_timestamp = getRTCClock()->getCurrentTime();   // line 85
uint32_t heard_seconds_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;  // line 358
uint32_t secs_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;  // line 1045

The neighbor table stores when each neighbor was last heard using the repeater's clock, and computes "seconds ago" relative to it. Since both values use the same clock, the relative "seconds ago" calculation remains internally consistent even if the absolute time is wrong. This data is only used for display/sorting in CLI and API responses -- it does NOT affect any forwarding decisions.

C. Clock sync responses (examples/simple_repeater/MyMesh.cpp:186-212):

uint32_t now = getRTCClock()->getCurrentTime();
memcpy(&reply_data[4], &now, 4);  // include our clock

When a companion device asks for the repeater's time via handleAnonClockReq, it gets the repeater's current RTC value. Companion devices that sync their clock from this repeater will inherit the wrong time.

D. Log file timestamps (examples/simple_repeater/MyMesh.cpp:440-447):

uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", ...);

Purely diagnostic.

E. Clock can't go backward (src/helpers/CommonCLI.cpp:225-253):

if (sender_timestamp > curr) {
  getRTCClock()->setCurrentTime(sender_timestamp + 1);
  ...
} else {
  strcpy(reply, "ERR: clock cannot go backwards");
}

The clock sync and time CLI commands refuse to set the clock backward. If a repeater's clock somehow gets stuck in the future (e.g. a bad sync from a companion with a wrong clock), the only fix is the clkreboot command (CommonCLI.cpp:213-216), which hard-resets the clock and reboots the device. This is a real operational headache if the repeater is on a mountaintop, but it still doesn't affect packet forwarding -- the repeater keeps repeating while its clock is wrong.


9. Replay protection -- not the repeater's problem

The replay protection in BaseChatMesh (src/helpers/BaseChatMesh.cpp:117) checks:

if (timestamp <= from->last_advert_timestamp) {
  // Possible replay attack
}

This compares the sender's timestamp in the advert against the sender's previous timestamp. It is a sender-clock-to-sender-clock comparison. The repeater's clock plays no role. And critically, this code path is in BaseChatMesh which the repeater does NOT extend -- the repeater extends mesh::Mesh directly (MyMesh.h:83). The repeater's onAdvertRecv() override (MyMesh.cpp:620-631) calls the base Mesh::onAdvertRecv() which is an empty virtual (Mesh.h:121), not the BaseChatMesh version.


Summary

AspectUses Repeater's Clock?Affects Mesh Behavior?
Flood repeat decisionNoN/A
Direct forward decisionNoN/A
Packet deduplicationNoN/A
Retransmit/reception delaysNo (uses millis)N/A
Loop detectionNoN/A
Region/transport filteringNoN/A
Forwarding other nodes' advertsNo (original payload preserved)N/A
Repeater's own advert timestampYesNo -- maps/tools display only
Neighbor table "seconds ago"YesNo -- display/API only, internally consistent
Clock sync to companionsYesNo -- doesn't change repeating behavior
Log timestampsYesNo -- diagnostic only
Clock-can't-go-backward guardYesNo -- management interface only

Verdict: A repeater with the wrong time will repeat every packet -- flood and direct -- exactly the same as one with the correct time. The wrong time only affects (1) what timestamp this repeater stamps on its own adverts (visible in MeshMapper/web tools), (2) clock sync responses to companion devices, and (3) display formatting of neighbor data. No routing, forwarding, filtering, deduplication, or repeat logic consults the RTC clock.


10. Addressing counterarguments

After publishing, this post was challenged with five counterarguments. I went back to the source code and verified each one.

  1. "Repeaters do anti-replay timestamp checks and drop old packets." Wrong. The anti-replay check (timestamp <= from->last_advert_timestamp) exists in BaseChatMesh.cpp:117. The repeater does not extend BaseChatMesh. It extends mesh::Mesh directly (MyMesh.h:83). The repeater's onAdvertRecv() does zero timestamp comparison -- it calls the base Mesh::onAdvertRecv() which is an empty virtual. This check simply does not exist in the repeater's code path.

  2. "If the clock resets to epoch, the repeater's adverts get rejected and it becomes a ghost." Partially true. Clients running BaseChatMesh may reject the repeater's own adverts if the timestamp goes backward. This is already covered in section 8A above. But this is about how the repeater appears on maps and tools -- it has nothing to do with whether the repeater forwards other nodes' packets. It does. Every single one.

  3. "A future clock bricks the repeater because the clock can't go backward." True, but doesn't affect forwarding. I've added this to section 8E above. If a repeater syncs to a bad clock source and gets stuck in the future, you need clkreboot to fix it. That's a real operational problem. But the repeater keeps forwarding packets the entire time its clock is wrong -- you just can't remotely fix the clock via normal commands until someone runs clkreboot.

  4. "Remote administration breaks when the clock resets." Wrong. The login replay check at MyMesh.cpp:114 compares sender_timestamp <= client->last_timestamp -- both values are the sender's timestamps, not the repeater's clock. The repeater's RTC is only used for activity tracking (line 121), not auth decisions. After a reboot, the client table clears and new logins work normally regardless of the RTC value.

  5. "Accurate time is required for advertisement intervals and duty cycle." Wrong. Advertisement intervals use futureMillis() (MyMesh.cpp:974), which calls _ms->getMillis() (Dispatcher.cpp:385) -- a monotonic uptime counter, not the RTC. The repeater will send adverts at correct intervals even if the RTC thinks it's 1970.

Other than the clock-can't-go-backward issue (#3), which I've incorporated into section 8E, none of these counterarguments change the thesis: a repeater's RTC clock has zero effect on whether it forwards packets.


This analysis was generated using Claude Opus 4.6 (1M context) pointed at the current MeshCore source. If you find an error, I want to hear about it. Bring code references.