Skip to content

Bitcoin accepted at checkout  |  Ships from Montreal, QC, Canada  |  Expert support since 2016

Meshtastic Store & Forward: The Offline Message History Reference

Quick answer

Store & Forward (S&F) is the Meshtastic module that lets one well-placed node keep a history of the mesh’s text messages so nodes that were off or out of range can ask for a replay when they return. A server (an ESP32 device in ROUTER/ROUTER_LATE role, or with is_server set, with at least 1 MB of free PSRAM) stores every text message it hears; a client requests history on the STORE_FORWARD_APP port (65) and receives up to 25 stored messages from the last 240 minutes by default, paced one packet per 5000 ms. Every number on this page is quoted from the firmware source.

Download CSV Download JSON REST API →

Sourced verbatim from meshtastic/firmware → src/modules/StoreForwardModule.cpp / .h and meshtastic/protobufs → storeforward.proto / module_config.proto / portnums.proto (as of 2026-07-22). Defaults, thresholds and enum values are exact literals from those files. Note that portnums.proto still flags STORE_FORWARD_APP as “(Work in Progress)” — behaviour can change between firmware releases; the “as of” date above is the contract.

How Store & Forward works

  • One node becomes the mesh’s memory. A server-capable node (ESP32 with PSRAM, in ROUTER/ROUTER_LATE role or with is_server set) listens promiscuously and copies every text message it hears into a PSRAM ring buffer (historyAdd()). Positions and telemetry are not stored — wantPacket() only accepts TEXT_MESSAGE_APP and STORE_FORWARD_APP.
  • Capacity is sized from PSRAM. Unless records is set, the buffer is sized to (((getFreePsram() / 4) * 3) / sizeof(PacketHistoryStruct)) — up to three-quarters of free PSRAM. When full, the server logs “S&F - PSRAM Full. Starting overwrite” and wraps around, overwriting the oldest records.
  • Clients ask for a replay. A client sends CLIENT_HISTORY (optionally with a window in minutes) on port 65; without a window the server uses its default of 240 minutes. The server answers with ROUTER_HISTORY (how many messages are coming, the window used, and last_request so repeat requests skip already-delivered packets), then replays each stored message as ROUTER_TEXT_BROADCAST or ROUTER_TEXT_DIRECT.
  • Replay is polite by design. At most historyReturnMax (default 25) messages per request, one packet per packetTimeMax (5000 ms), and only while the channel-utilization gate passes (the code comment caps it at “less than 25% utilized”). Replayed packets go out at BACKGROUND priority with want_ack = false.
  • The replay filter protects privacy boundaries. A client only ever receives broadcasts or direct messages addressed to it, never its own messages and never other nodes’ DMs — and history requests on the default public channel are refused outright.

Every setting, default and protocol code

GroupItemValue / #Unit / typeSourceDescription
Requirement Supported architectures ARCH_ESP32, ARCH_PORTDUINO compile guard StoreForwardModule.cpp The whole module body is wrapped in "#if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO)" — S&F only exists on ESP32 devices and the Linux-native (Portduino) build. nRF52 devices cannot run it.
Requirement Server role gate ROUTER, ROUTER_LATE, or is_server device role StoreForwardModule.cpp Server mode initializes only when config.device.role is ROUTER or ROUTER_LATE, or moduleConfig.store_forward.is_server is set. Every other enabled node becomes a client (is_client = true).
Requirement PSRAM presence memGet.getPsramSize() > 0 condition StoreForwardModule.cpp A server-mode device without PSRAM logs "S&F: device doesn't have PSRAM, Disable" and disables the module.
Requirement Minimum free PSRAM 1024 * 1024 bytes (1 MB) StoreForwardModule.cpp Server startup requires memGet.getFreePsram() >= 1024 * 1024; below that it logs "S&F: not enough PSRAM free, Disable".
Firmware default historyReturnMax 25 records StoreForwardModule.h "Return maximum of 25 records by default." — the most messages a server will replay per history request unless history_return_max overrides it.
Firmware default historyReturnWindow 240 minutes (4 h) StoreForwardModule.h "Return history of last 4 hours by default." — the replay window used when a client does not specify one, unless history_return_window overrides it.
Firmware default records 0 (auto-calculated) records StoreForwardModule.h Defaults to 0, meaning populatePSRAM() computes capacity as (((memGet.getFreePsram() / 4) * 3) / sizeof(PacketHistoryStruct)) — up to 3/4 of free PSRAM.
Firmware default heartbeat false bool StoreForwardModule.h "No heartbeat." — the server broadcasts no heartbeat unless moduleConfig.store_forward.heartbeat enables it.
Firmware default heartbeatInterval 900 seconds (15 min) StoreForwardModule.h When heartbeat is enabled, the server broadcasts a ROUTER_HEARTBEAT every heartbeatInterval seconds (checked as heartbeatInterval * 1000 ms); the period is echoed in variant.heartbeat.period.
Firmware default packetTimeMax 5000 ms StoreForwardModule.h "Interval between sending history packets as a server." — runOnce() paces replay so stored messages go out at most one per 5 seconds.
Firmware behaviour Channel-utilization gate isTxAllowedChannelUtil(true) condition StoreForwardModule.cpp Replay and heartbeat transmissions only happen when airtime permits: "Only send packets if the channel is less than 25% utilized and until historyReturnMax".
Firmware behaviour Stored payload types TEXT_MESSAGE_APP only PortNum StoreForwardModule.cpp historyAdd() runs on TEXT_MESSAGE_APP packets — the history holds text messages, not positions or telemetry. wantPacket() accepts only TEXT_MESSAGE_APP and STORE_FORWARD_APP.
Firmware behaviour History buffer overwrite wraps to 0 when full ring buffer StoreForwardModule.cpp When packetHistoryTotalCount reaches records the server logs "S&F - PSRAM Full. Starting overwrite", resets the counter to 0 and starts overwriting the oldest slots.
Firmware behaviour Replay filter from != dest && (to == BROADCAST || to == dest) condition StoreForwardModule.cpp A client is only sent packets it did not author itself, and only broadcasts or direct messages addressed to it.
Firmware behaviour Public-channel refusal "S&F not permitted on the public channel." text reply StoreForwardModule.cpp History requests arriving on the default (public) channel are refused with this text message (channels.isDefaultChannel check).
Firmware behaviour Busy refusal "S&F - Busy. Try again shortly." text reply StoreForwardModule.cpp While the server is replaying to one client it answers other history requests with this text message; CLIENT_STATS gets a ROUTER_BUSY instead.
Firmware behaviour Client retry delay available_packets * packetTimeMax * (2 if ROUTER_ERROR else 1) ms StoreForwardModule.cpp On ROUTER_BUSY / ROUTER_ERROR a client computes retry_delay = millis() + getNumAvailablePackets(...) * packetTimeMax, doubled for ROUTER_ERROR.
Firmware behaviour Legacy text trigger "SF" + 0x00 text message StoreForwardModule.cpp A direct text message whose payload starts with bytes 'S', 'F', 0x00 is a "Legacy Request to send" — the server replays historyReturnWindow * 60 seconds of history to the sender.
Firmware behaviour Replay want_ack false bool StoreForwardModule.cpp Replayed and protocol packets are sent with want_ack = false and priority BACKGROUND: "Let's assume that if the server received the S&F request that the client is in range."
Config field enabled 1 bool module_config.proto "Enable the Store and Forward Module" — off by default, like other Meshtastic modules.
Config field heartbeat 2 bool module_config.proto Enables the periodic ROUTER_HEARTBEAT broadcast on a server (firmware default: off).
Config field records 3 uint32 module_config.proto Maximum number of records to store in memory; 0 lets the firmware auto-size from free PSRAM.
Config field history_return_max 4 uint32 module_config.proto Overrides the maximum number of records returned per history request (firmware default 25).
Config field history_return_window 5 uint32 module_config.proto Overrides the history window in minutes (firmware default 240 = 4 hours).
Config field is_server 6 bool module_config.proto "Set to true to let this node act as a server that stores received messages and resends them upon request." — lets a non-router role run the server.
Protocol STORE_FORWARD_APP 65 PortNum portnums.proto The S&F protocol port — flagged "(Work in Progress)" in portnums.proto. See the PortNum registry.
RequestResponse UNSET 0 enum storeforward.proto Unset/unused.
RequestResponse ROUTER_ERROR 1 enum storeforward.proto Router is in an error state. Codes 001–063 are from the router.
RequestResponse ROUTER_HEARTBEAT 2 enum storeforward.proto Router heartbeat — carries a Heartbeat variant with period (seconds) and secondary (0 = primary router).
RequestResponse ROUTER_PING 3 enum storeforward.proto Router has requested the client respond; works as an "are you there" message.
RequestResponse ROUTER_PONG 4 enum storeforward.proto The response to a "Ping". A client treats it like receiving a heartbeat.
RequestResponse ROUTER_BUSY 5 enum storeforward.proto Router is currently busy; please try again later.
RequestResponse ROUTER_HISTORY 6 enum storeforward.proto Router is responding to a request for history — carries history_messages (count), window and last_request.
RequestResponse ROUTER_STATS 7 enum storeforward.proto Router is responding to a request for stats — carries the Statistics variant.
RequestResponse ROUTER_TEXT_DIRECT 8 enum storeforward.proto Router replays a stored text message that was a direct message.
RequestResponse ROUTER_TEXT_BROADCAST 9 enum storeforward.proto Router replays a stored text message that was a broadcast.
RequestResponse CLIENT_ERROR 64 enum storeforward.proto Client is in an error state. Codes 064–127 are from the client; the server aborts an in-progress replay to that client.
RequestResponse CLIENT_HISTORY 65 enum storeforward.proto Client has requested a replay from the router — may carry a History variant whose window is in minutes.
RequestResponse CLIENT_STATS 66 enum storeforward.proto Client has requested stats from the router.
RequestResponse CLIENT_PING 67 enum storeforward.proto Client has requested the router respond; works as an "are you there" message.
RequestResponse CLIENT_PONG 68 enum storeforward.proto The response to a "Ping".
RequestResponse CLIENT_ABORT 106 enum storeforward.proto Client has requested that the router abort processing the client's request.
Message field rr 1 RequestResponse storeforward.proto The request/response code — every S&F packet carries one.
Message field stats (oneof variant) 2 Statistics storeforward.proto Server statistics: messages_total, messages_saved, messages_max, up_time (s), requests, requests_history, heartbeat, return_max, return_window (minutes).
Message field history (oneof variant) 3 History storeforward.proto History metadata: history_messages (count to be sent), window (the filter window used) and last_request (index of the last message previously sent, so a client can avoid duplicates).
Message field heartbeat (oneof variant) 4 Heartbeat storeforward.proto Heartbeat payload: period (seconds between heartbeats) and secondary ("If set, this is not the primary Store & Forward router on the mesh").
Message field text (oneof variant) 5 bytes storeforward.proto Text from a replayed history message (used with ROUTER_TEXT_DIRECT / ROUTER_TEXT_BROADCAST).

Requesting history in practice

  • From a client app: send a StoreAndForward protobuf with rr = CLIENT_HISTORY (65) on STORE_FORWARD_APP. Include a history.window in minutes to override the server’s 4-hour default. Use history.last_request from the previous ROUTER_HISTORY reply to skip messages you already received.
  • Legacy trigger: a direct text message whose payload starts with the bytes ‘S’, ‘F’, 0x00 also triggers a replay of the default window.
  • Liveness: CLIENT_PINGROUTER_PONG confirms a server is reachable; if the server heartbeat is enabled it broadcasts ROUTER_HEARTBEAT every 900 s by default.
  • Health: CLIENT_STATSROUTER_STATS returns messages_saved vs messages_max, uptime, request counters and the server’s active return_max/return_window.
  • If the server is busy replaying to someone else you get the busy text (or ROUTER_BUSY for stats); the client backs off by available_packets × 5000 ms, doubled after a ROUTER_ERROR.

Limits and caveats

  • Text messages only. The history stores TEXT_MESSAGE_APP payloads — no positions, telemetry or other ports.
  • ESP32 (or Linux-native) with PSRAM only. The module body compiles only for ARCH_ESP32 / ARCH_PORTDUINO, and server mode needs ≥ 1 MB free PSRAM — nRF52 nodes and PSRAM-less boards cannot serve.
  • RAM, not flash. The ring buffer lives in PSRAM — a server reboot loses the stored history.
  • One server per mesh is the design intent. The heartbeat’s secondary field exists, but the firmware comments “we always have one primary router for now”.
  • Not on the public channel. History requests on the default channel are refused; run S&F on a private channel.
  • Work in progress. portnums.proto flags the port “(Work in Progress)”; validate against the firmware version you actually run.

Why this matters for sovereign off-grid comms

A mesh whose messages evaporate the moment a node sleeps is a walkie-talkie network; a mesh with Store & Forward is an answering machine you own. For the resilient-communications layer of a sovereignty stack, that difference is structural: a solar-powered router node on high ground with S&F enabled means a handset that was powered off during an emergency broadcast can rejoin and ask “what did I miss?” — with no carrier, no cloud and no account. It is the same self-custody principle we apply to Bitcoin infrastructure: the history of your communications should live on hardware you control. See also the store-and-forward glossary entry for the general networking concept, and mesh emergency communications in Canada for deployment context.

Related

Meshtastic PortNum registry (port 65 lives here) · MeshPacket & Data field reference (the envelope S&F packets ride in) · Meshtastic node roles (ROUTER / ROUTER_LATE) · LoRa modem presets · Meshtastic over MQTT (the internet-bridged alternative).

Credit: Store & Forward is designed and maintained by the Meshtastic project — module code in meshtastic/firmware and protocol schema in meshtastic/protobufs (GPL-3.0). This page is a dated, human- and machine-readable index of those files.