Chapter 7

Delay, Trigger, context, and state persistence

Timers can accumulate messages, while state can span multiple messages. Both need explicit limits and restart semantics. You will use Delay to control message timing, Trigger to create cancelable time windows, and node/flow/global context to store only the bounded data you need.

Why this matters

“Turn off the light two minutes after motion is detected” involves timing, repeated events, cancellation, and restart behavior. “How many notifications have been sent today?” involves state scope and persistence. A simple Delay can queue one future message for every motion event, while pushing every event into a global array causes memory use to grow indefinitely. Start by asking: Does each room need only one countdown? Should a new event reset the timer, enter a queue, or be discarded? After a restart, should the flow restore state, recalculate it, or stop safely?

This chapter uses the Delay and Trigger nodes and runtime context in Node-RED 5.0.2. Every manual test ends at a Debug node, not a Home Assistant Action node. Lights and notifications are design scenarios only. Chapter 12 and Chapter 13 place timing and event nodes in controlled Home Assistant flows.

Concept: A timer queue, Trigger's pending topics, and memory context are not durable job schedulers. They do not guarantee execution across restarts. Even a localfilesystem context store does not automatically preserve a timer that is counting down inside a node.

Core concepts

  • Delay: Delays can be fixed, dynamic, or random, and the node can also limit the message rate. Queueing, dropping, and using a second output are distinct policies. Messages waiting in a queue consume resources.
  • Trigger: The node can send one value immediately and another after a delay. New messages can extend the countdown. You can use one timer for all messages or separate timers by a specified message property, and you can reset those timers.
  • node context: This scope belongs to one node instance. Use it for that node's counter, previous value, or other small state.
  • flow context: Nodes on the same flow tab share this scope. Use it for state that coordinates one automation. Subflows and their parent scope introduce additional boundaries, covered in Chapter 17.
  • global context: Every flow can access this scope, so it creates the broadest coupling and exposure. Use it only for data that truly must cross flows and has a clear owner.
  • context store: Scope answers "who can access this value?" A store answers "where is it stored?" Do not confuse memory with localfilesystem.

Context is key/value state, not an event database. Storing the current number of consecutive failures is usually reasonable. Storing complete sensor history, an entire state object, HTTP request/response objects, or an access token is not. If you need history, use an external data system with a retention policy, and limit both the query and the returned fields.

Manually verify the context counter example

  1. Check the example's boundaries.

    Read 03-context-counter.json. It contains only Inject, Change, and Debug nodes. The Change node uses JSONata to update count in flow context, then puts the value in msg.payload.

  2. Import it into a separate test tab.

    Before importing, confirm that the example contains no Home Assistant server, network node, credential, or automatic schedule. Leave the Inject node's repeat setting blank and trigger it manually with the button.

  3. Verify the first and subsequent triggers.

    The first output should be 1, followed by 2 and 3. This proves that the same flow context can be read and written across messages. It does not prove that the value persists across restarts.

  4. Test the scope boundary.

    Copy the Change and Debug nodes to another tab. Whether you keep or rename the key, observe that flow context is not shared across tabs. Before changing the scope to global, understand that doing so broadens visibility. Delete the key after the test.

  5. Test the restart contract.

    Record the current count, then restart Node-RED through your environment's approved maintenance process. If no durable store is configured, expect the memory value to disappear. If a named store is configured, test its actual behavior rather than inferring durability from its name.

JSONata expressions and Change rules can both read and write context. Concurrent read-modify-write operations require additional design when inputs are frequent or arrive in parallel. This example uses one manually operated Inject node and is not presented as a high-concurrency counter.

Delay and Trigger: Choose between queueing and resetting

In the Node-RED 5.0.2 runtime, Delay supports a fixed delay, a dynamic delay overridden by msg.delay, a random delay, and several rate-limiting paths. In delay mode, a message with reset can clear waiting messages, while flush can release them. Rate mode can queue or drop messages and is constrained by the runtime's nodeMaxMessageBufferLength. These properties are control interfaces; do not let untrusted input set arbitrary delay, rate, reset, or flush values.

For example, notification throttling might mean “send one per minute and queue the rest” or “keep only the latest update and drop intermediate updates.” Queueing is appropriate when every task matters and the total is bounded; dropping is appropriate for telemetry that the next value supersedes. Emergency alerts should not be held up by a shared Delay queue. Rate limiting controls cadence only: it does not confirm downstream success or replace retries and backoff.

Trigger 5.0.2 can use one timer for all messages or maintain separate timers by a specified msg property, commonly topic. With extended delay enabled, each new message restarts the wait; msg.reset can cancel the corresponding timer. This is closer to “send a turn-off candidate only after the last motion event,” but before actually turning off a light, query its current state and check occupancy again. Do not treat a two-minute-old message as a current fact.

RequirementBetter fitRequired safeguards
Delay every message by the same intervalDelayLimit queue length and input rate
Control the rate of API callsDelay rate limitTimeout, error branch, and queue policy
Output only after the last messageTrigger + extendSeparate topics by room and provide reset
Send a start value now and an end value laterTriggerDefine repeated-input and restart semantics

When timers are separated by topic, the set of topics must be bounded. If an attacker or faulty source can continuously generate new topics, Trigger's topic map will accumulate pending timers. First apply a Switch allowlist or map unknown sources to a fixed quarantine topic.

node, flow, and global: Choose the narrowest scope

Choose a scope by asking one question: “Which nodes need to read this value?” Use node when only the current node needs it, flow for coordination within the same tab, and global only for clearly defined shared data across tabs. The broader the scope, the harder it is to track ownership, initialization, and cleanup—and the easier it is for an unrelated flow to overwrite the value.

StateRecommended scopeRationaleLimit or cleanup
A node's consecutive error countnodeNo sharing requiredSet a maximum and reset to zero on recovery
Notification suppression flag for one room's flowflowSeveral nodes coordinateUse a Boolean or small timestamp with an explicit reset
Site-wide maintenance modeglobal (use cautiously)Multiple flows need to read itSingle owner, safe default, and change log
Complete sensor historyDo not use contextUnbounded, with different query needsUse a managed data system and retention policy

Names should describe purpose rather than reveal environmental secrets—for example, notificationCount or maintenanceMode. Avoid vague names such as data and temp, and do not include an address, user name, or device ID in a key. Deleting a flow or moving a node can change the identity of its node or flow scope. Rerun initialization tests after upgrades and refactoring.

A Change node's typed input or the Function API can specify a named store. If that store does not exist, the runtime logs an unknown-store warning and falls back to the default store. Do not mistake this fallback for successful persistence. Before deployment, verify the settings and runtime log, then test the actual restart behavior.

Durability boundaries of memory and localfilesystem

When no contextStorage plugin is configured, the Node-RED 5.0.2 runtime uses the memory store. Memory is fast and suitable for transient values, but values cannot be expected to survive a process restart. To use localfilesystem, configure it explicitly in the runtime settings. This chapter does not directly modify the Add-on's shared settings: a configuration error would affect every flow, so back up first and review the change during a maintenance window.

localfilesystem enables its memory cache by default. Its source-defined default flushInterval is 30 seconds, meaning “the minimum interval between writes to storage,” which reduces wear on the underlying storage. A set operation first updates the cache and marks a write as pending; a timer flushes it later, while a normal close attempts a flush. Therefore, each assignment is not durable immediately. A sudden power loss or process crash can lose recent updates that have not yet been flushed.

Saving to disk is not a database transaction or a backup. Serialization logs a warning when it encounters a circular reference; live objects, functions, and values unsuitable for JSON should not be stored. A full disk, permission errors, file corruption, and backup or restore failures can all cause data loss. For safety-critical state, make Home Assistant or a dedicated data service the authoritative source and verify it again when the flow starts rather than trusting old context blindly.

// Conceptual configuration snippet for review only; do not apply it without a backup
contextStorage: {
  default: "memoryOnly",
  memoryOnly: { module: "memory" },
  durable: { module: "localfilesystem" }
}

If your environment approves this configuration, keep high-frequency transient counters in memory and assign only the few small values that truly need to survive restarts to the durable store. After making the change, check the runtime startup log and the store shown in the Context sidebar. Test both contracts: “write → allow enough time to flush → normal restart → verify” and “persistence is not guaranteed after an abnormal interruption.” Do not reduce the flush interval and claim zero data loss; doing so increases write frequency without providing transaction guarantees.

Bounded state, startup policies, and a manual test matrix

Every long-running flow needs four limits: the maximum number of pending messages, the maximum number of timers or topics, the maximum context value size, and the maximum state lifetime. Enforce these limits at the input rather than relying only on available host memory. Send over-limit cases to a Catch, Status, or diagnostic branch, clear the payload, and then record the count. Do not write complete household events to the log.

TestProcedureExpected observation
Single messageManually Inject one messageStart value, wait duration, and end value
Repeated messageInject again during the waitWhether the message is queued, dropped, or extends the timer
CancellationSend reset with a fixed topicOnly the expected timer is cleared, with no end output
Parallel timersInterleave two fixed topicsWhether timers remain isolated and the topic map stays bounded
RestartPerform an approved restart during the countdownThe pending timer is not assumed to recover; startup enters a safe state
ContextRestart after writing a valueMemory state disappears; the configured file store is verified against its contract
Over limitTest the limit with a small, controlled burstObservable rejection without creating an unbounded queue

For entrance lighting, a safe startup policy might be: “After a restart, do not resend an old turn-off command. Wait for the next event, then query current occupancy and light state.” A notification counter can reset at the daily boundary and have a maximum value rather than incrementing without limit. Daylight saving time, time zones, and scheduling belong in the chapter on Home Assistant time nodes; do not use Delay's millisecond wait as calendar scheduling.

Before deployment, simulate with a disabled Action node or Debug alone. After the tests pass, every downstream node with real side effects still needs a state query, allowlist, error handling, and a manual recovery method. Do not stress-test the production Add-on with large numbers of Inject messages. Run resource tests in an isolated environment with defined stop conditions.

Troubleshooting

  • Every motion event triggers an action later, so actions accumulate: You may be queueing with Delay when the requirement calls for Trigger with extend. First disconnect the side-effect output and clear the queue, then use repeated manual inputs to verify the reset semantics.
  • Trigger reset has no effect: Check whether timers are separated by topic and whether the reset message's property exactly matches the original message. Do not let unknown topics create timers arbitrarily.
  • The count returns to zero after a restart: Confirm the current store. Without a contextStorage configuration, context uses memory. If the value truly must survive restarts, back up and review the system before configuring a named localfilesystem store, then perform a restart test.
  • A recent file-store write is still lost after a power failure: This is consistent with the cache and flush boundary; an assignment is not immediately durable on disk. Do not promise zero data loss. Instead, let an authoritative system reconstruct safety-critical state.
  • The Context sidebar shows a value, but a Function node cannot read it: Confirm that the node/flow/global scope, tab, key, and named store all match. Check the runtime for an unknown-store warning.
  • Memory or storage keeps growing: Check the Delay queue, Trigger topics, pending Join operations, and context arrays. Stop the source, set limits and TTLs, and back up any necessary de-identified diagnostics before cleanup.

Pinned sources

After configuring file storage, test in this order: “write → wait for flush → normal restart → verify.” A sudden interruption can still lose recent updates that have not yet been written to disk.

FAQ

Repeated events produce several notifications in a short period. Should I use Delay or Trigger?
If you need to limit the rate or send messages in sequence, evaluate Delay first. If each new event should extend or reset the same time window, evaluate Trigger. In either case, limit the number of queued messages or topics and connect Debug first to test bursts, resets, and timeouts.
Does flow.get("exampleKey") inside a Subflow read from the parent tab?
No. A Subflow has its own flow context. Only when the design genuinely needs to cross that boundary should you explicitly use flow.get("$parent.exampleKey"), as defined by the Node-RED context API, to access the parent flow's context. Document the owner, default value, and cleanup policy. Otherwise, keep the value inside the Subflow to avoid hidden coupling. Chapter 17 covers the complete architecture.
Does localfilesystem guarantee that no set operation is ever lost?
No. The default cache mode updates memory first and writes to disk according to the flush interval. A sudden interruption can lose recent pending writes. It is also neither a database transaction nor a backup.
Can I put a complete Home Assistant state object in global context?
It is not recommended. Doing so expands privacy exposure and coupling, and the data can grow without bounds. Store only necessary, de-identified, small derived values with size and expiry limits. Query the authoritative source again when you need current state.
Will Trigger continue its countdown after a restart?
Do not assume so. A pending timer inside a node and a context store are separate mechanisms. Define a safe startup policy—for example, discard old candidates, wait for a new event, and query current state again.