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_LATErole or withis_serverset) listens promiscuously and copies every text message it hears into a PSRAM ring buffer (historyAdd()). Positions and telemetry are not stored —wantPacket()only acceptsTEXT_MESSAGE_APPandSTORE_FORWARD_APP. - Capacity is sized from PSRAM. Unless
recordsis 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 awindowin minutes) on port 65; without a window the server uses its default of240minutes. The server answers withROUTER_HISTORY(how many messages are coming, the window used, andlast_requestso repeat requests skip already-delivered packets), then replays each stored message asROUTER_TEXT_BROADCASTorROUTER_TEXT_DIRECT. - Replay is polite by design. At most
historyReturnMax(default 25) messages per request, one packet perpacketTimeMax(5000 ms), and only while the channel-utilization gate passes (the code comment caps it at “less than 25% utilized”). Replayed packets go out atBACKGROUNDpriority withwant_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
| Group | Item | Value / # | Unit / type | Source | Description |
|---|---|---|---|---|---|
| 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
StoreAndForwardprotobuf withrr = CLIENT_HISTORY(65) onSTORE_FORWARD_APP. Include ahistory.windowin minutes to override the server’s 4-hour default. Usehistory.last_requestfrom the previousROUTER_HISTORYreply to skip messages you already received. - Legacy trigger: a direct text message whose payload starts with the bytes
‘S’, ‘F’, 0x00also triggers a replay of the default window. - Liveness:
CLIENT_PING→ROUTER_PONGconfirms a server is reachable; if the server heartbeat is enabled it broadcastsROUTER_HEARTBEATevery 900 s by default. - Health:
CLIENT_STATS→ROUTER_STATSreturnsmessages_savedvsmessages_max, uptime, request counters and the server’s activereturn_max/return_window. - If the server is busy replaying to someone else you get the busy text (or
ROUTER_BUSYfor stats); the client backs off byavailable_packets × 5000 ms, doubled after aROUTER_ERROR.
Limits and caveats
- Text messages only. The history stores
TEXT_MESSAGE_APPpayloads — 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
secondaryfield 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.protoflags 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.
Related products, repair, and setup paths
- Bitcoiner sovereignty hub
- the plebs sovereign stack
- Nostr for Bitcoiners
- run your own Nostr relay
- getting started with Meshtastic
- Bitcoin over Meshtastic mesh networks
- open-source hardware tools directory
- off-grid Bitcoin mining
Last reviewed July 22, 2026.
