Skip to content

Files and Context

Scope: Node-RED's two kinds of memory — files on the disk, which it reads and writes like any program, and context, its own notepad — including which one survives a restart, proved by restarting.

Writing and reading files

Two nodes do the work. The file node writes msg.payload to a named file — append or overwrite, your choice per node. The file in node reads a file into msg.payload.

The lab's tab 4 chains both directions of travel:

[inject "the kettle has boiled"]
   ─▶ [function: stamp the time] ─▶ [file: append] ─▶ [debug]

[http in /notes] ─▶ [file in] ─▶ [http response]

After two presses, the file on disk — checked with plain cat, not taken on Node-RED's word:

2026-10-22T23:40:52.241Z - the kettle has boiled
2026-10-22T23:40:52.288Z - the kettle has boiled

and GET /notes returned those same two lines to the browser (t01-t08-tests.log). That is the whole pattern behind a family of useful flows: log lines, daily reports, a CSV of sensor readings — things happen, a function formats them, a file keeps them. Two honest warnings:

  • Paths are the machine's paths. A file node writes as the user Node-RED runs as, to a path on that machine. Relative names resolve against the folder Node-RED started from, which is a fine way to lose a file; the lab used an absolute path, and so should you.
  • Appending is not a database. Two flows writing one file at the same moment can interleave. For a household log this is a shrug; for anything that matters, it is chapter 14's limit.

Context: the notepad

Sometimes a flow needs to remember between messages: a counter, the last reading, "did I already send that alert". That is context, in three scopes:

Scope Shared by Set with
node one node's calls context.get/set in a function
flow every node on the tab flow.get/set
global the whole runtime global.get/set

The lab's tab 6 counts presses in flow context and serves the total over HTTP:

let n = flow.get("presses") || 0;
n = n + 1;
flow.set("presses", n);
msg.payload = { presses: n };
return msg;

Three presses, and GET /presses said {"presses":3} (t01–t08). Then the lab restarted Node-RED and asked again: {"presses":0} (transcript t09-restart1.log).

That is the default: context lives in memory and a restart forgets it. The flows themselves — all 37 nodes — came back untouched, because flows are a file. Context is not, until you tell it to be.

Making context survive

One settings block (chapter 9 shows the file it lives in) changes the store:

contextStorage: {
    default: {
        module: "localfilesystem",
        config: { cache: true }
    }
}

With that set, the startup log names the store — [info] Context store : 'default' [module=localfilesystem] — and the lab re-ran the experiment: two presses, a restart, and GET /presses still said {"presses":2} (t10, t11 — the second restart was a full systemctl restart of the service for good measure). Context is kept as small files under the user directory's context/ folder.

Persistent context is still a notepad, not a ledger: values are written on a short delay, so a hard power cut can lose the last few seconds, and there is no transaction keeping two related values consistent. Counters, last-known states, and "already notified" flags are its natural citizens.