Skip to main content

ControlForge Resilience & Caching Guide

James M. Belcher Founder, JMB Technical Services LLC April 2026 | ControlForge v1.0.535


1. Overview

ControlForge provides 35 built-in functions for building fault-tolerant, production-hardened control systems. These patterns protect against network failures, noisy inputs, resource exhaustion, and cascading faults — all callable from Structured Text.

PatternFunctionsUse Case
Cache12TTL-based key-value cache with LRU eviction
Circuit Breaker7Stop calling a failing service, auto-recover
Rate Limiter4Cap requests per time window
Throttle3Enforce minimum interval between calls
Debounce3Ignore rapid repeated triggers
Bulkhead5Limit concurrent operations
Fallback1Default value for falsy inputs
Hysteresis1Dead-band to prevent signal chatter
Rate Limit (Analog)1Clamp rate of change on analog values

All handle-based patterns (Cache, Circuit Breaker, Rate Limiter, Throttle, Debounce, Bulkhead) use string handles — create once, reference by name.


2. Cache

TTL-based key-value cache with optional LRU eviction. Use for caching expensive calculations, API responses, or sensor averaging.

Create

(* Unlimited cache, no default TTL *)
c := CACHE_CREATE();

(* Max 100 entries, 60-second default TTL *)
c := CACHE_CREATE(100, 60);
ParamTypeDefaultDescription
max_sizeINT0 (unlimited)Maximum entries (LRU eviction when exceeded)
default_ttlINT0 (no expiry)Default TTL in seconds

Store and Retrieve

CACHE_SET(c, 'api_result', response_body); (* Default TTL *)
CACHE_SET(c, 'sensor_avg', avg_temp, 30); (* 30s TTL *)

val := CACHE_GET(c, 'api_result'); (* Returns value or nil *)
val := CACHE_GET(c, 'missing_key', 0.0); (* Returns 0.0 if not found *)

(* Get or compute: returns cached value, or stores default if missing *)
val := CACHE_GET_OR_SET(c, 'expensive_calc', computed_value, 60);

Manage

IF CACHE_HAS(c, 'api_result') THEN ... END_IF;
remaining := CACHE_TTL(c, 'api_result'); (* Seconds remaining, -1=no expiry, -2=not found *)
CACHE_EXPIRE(c, 'api_result', 10); (* Reset TTL to 10s *)
CACHE_DELETE(c, 'api_result');
removed := CACHE_CLEANUP(c); (* Remove expired entries, returns count *)
keys := CACHE_KEYS(c);
count := CACHE_SIZE(c);
CACHE_CLEAR(c);

Example: Cache API Responses

PROGRAM POU_CachedWeather
VAR
cache : STRING;
temp : REAL;
body : STRING;
initialized : BOOL := FALSE;
END_VAR

IF NOT initialized THEN
cache := CACHE_CREATE(50, 300); (* 5-minute default TTL *)
initialized := TRUE;
END_IF;

(* Check cache first *)
temp := CACHE_GET(cache, 'outdoor_temp', -999.0);

IF temp = -999.0 THEN
(* Cache miss — fetch from API *)
body := HTTP_GET_BODY('http://weather.local/temp');
IF LEN(body) > 0 THEN
temp := STRING_TO_REAL(body);
CACHE_SET(cache, 'outdoor_temp', temp, 300);
END_IF;
END_IF;
END_PROGRAM

3. Circuit Breaker

Prevents repeated calls to a failing service. After a threshold of failures, the breaker "opens" and blocks calls for a timeout period, then allows a few test calls ("half-open") before fully closing.

States

Create

cb := CIRCUIT_BREAKER_CREATE(5, 2, 30);
ParamTypeDefaultDescription
failure_thresholdINT5Failures before opening
success_thresholdINT2Successes in half-open before closing
timeout_secondsINT30Time in open state before half-open

Use Pattern

IF CIRCUIT_BREAKER_ALLOW(cb) THEN
(* Attempt the operation *)
resp := HTTP_GET('http://10.0.0.50/api/data');

IF HTTP_OK(resp) THEN
CIRCUIT_BREAKER_RECORD_SUCCESS(cb);
(* Process response... *)
ELSE
CIRCUIT_BREAKER_RECORD_FAILURE(cb);
END_IF;
ELSE
(* Circuit is open — use cached/default data *)
DEBUG_WARN('comms', 'Circuit breaker open — using cached data');
END_IF;

Monitor

state := CIRCUIT_BREAKER_STATE(cb); (* "closed", "open", or "half-open" *)
stats := CIRCUIT_BREAKER_STATS(cb); (* JSON with failures, successes, thresholds *)
CIRCUIT_BREAKER_RESET(cb); (* Force back to closed *)

4. Rate Limiter

Caps the number of operations within a sliding time window. Use for API call limits, alarm rate limiting, or log throttling.

rl := RATE_LIMITER_CREATE(10, 60); (* 10 requests per 60 seconds *)

IF RATE_LIMITER_ALLOW(rl) THEN
(* Within budget — proceed *)
MQTT_PUBLISH('telemetry', 'plant/data', payload);
ELSE
(* Rate exceeded — skip or queue *)
remaining := RATE_LIMITER_REMAINING(rl);
END_IF;
ParamTypeDefaultDescription
max_requestsINT100Maximum operations per window
window_secondsINT60Sliding window duration
FunctionReturnsDescription
RATE_LIMITER_CREATE(max, window)STRINGCreate limiter
RATE_LIMITER_ALLOW(h)BOOLCheck and consume quota
RATE_LIMITER_REMAINING(h)INTRemaining quota
RATE_LIMITER_RESET(h)BOOLClear all counters

5. Throttle

Enforces a minimum time interval between operations. Unlike rate limiter (which allows bursts up to a quota), throttle ensures even spacing.

th := THROTTLE_CREATE(5000); (* Minimum 5 seconds between calls *)

IF THROTTLE_ALLOW(th) THEN
(* At least 5s since last allowed call *)
send_email_alert();
END_IF;

wait_ms := THROTTLE_WAIT_TIME(th); (* Milliseconds until next allowed *)
FunctionReturnsDescription
THROTTLE_CREATE(interval_ms)STRINGCreate throttle (default: 1000ms)
THROTTLE_ALLOW(h)BOOLCheck if interval has elapsed
THROTTLE_WAIT_TIME(h)INTMilliseconds until next allowed

6. Debounce

Triggers only after a quiet period — ignores rapid repeated activations. Use for noisy digital inputs, button presses, or alarm suppression.

db := DEBOUNCE_CREATE(500); (* 500ms debounce delay *)

(* Call on every scan where input is active *)
IF button_pressed THEN
DEBOUNCE_CALL(db);
END_IF;

(* Only true after 500ms of quiet *)
IF DEBOUNCE_READY(db) THEN
(* Stable press detected — execute once *)
toggle_output := NOT toggle_output;
END_IF;
FunctionReturnsDescription
DEBOUNCE_CREATE(delay_ms)STRINGCreate debouncer (default: 250ms)
DEBOUNCE_CALL(h)BOOLRecord activation (resets timer)
DEBOUNCE_READY(h)BOOLTRUE if delay elapsed since last call

7. Bulkhead

Limits the number of concurrent operations — a semaphore pattern. Prevents resource exhaustion when multiple tasks or connections compete.

bh := BULKHEAD_CREATE(5); (* Max 5 concurrent operations *)

IF BULKHEAD_ACQUIRE(bh) THEN
(* Got a slot — do work *)
resp := HTTP_POST('http://api.example.com/data', payload);
BULKHEAD_RELEASE(bh); (* Always release when done *)
ELSE
(* All slots busy — reject or queue *)
available := BULKHEAD_AVAILABLE(bh);
END_IF;
FunctionReturnsDescription
BULKHEAD_CREATE(max_concurrent)STRINGCreate bulkhead (default: 10)
BULKHEAD_ACQUIRE(h)BOOLTake a slot (FALSE if full)
BULKHEAD_RELEASE(h)BOOLRelease a slot
BULKHEAD_AVAILABLE(h)INTRemaining slots
BULKHEAD_STATS(h)MAP{max_concurrent, current, available}

8. Fallback

Returns a default value when the primary value is falsy (nil, empty string, FALSE, 0, 0.0).

(* If sensor_reading is 0 or nil, use 72.5 *)
temp := FALLBACK(sensor_reading, 72.5);

(* If API response is empty, use cached value *)
data := FALLBACK(HTTP_GET_BODY(url), cached_data);

(* Chain fallbacks *)
value := FALLBACK(primary, FALLBACK(secondary, default_val));

9. Hysteresis

Dead-band function that prevents output chatter when an input oscillates near a threshold. The output only changes when the input crosses the high or low threshold — it holds its previous state in the dead band between them.

(* Heater control with 2-degree dead band *)
heater_on := HYSTERESIS(temperature, 68.0, 72.0, heater_on);
(* ON when temp drops below 68, OFF when temp rises above 72 *)
(* Holds previous state between 68-72 *)
ParamTypeDescription
inputREALCurrent value
low_thresholdREALTurn ON below this
high_thresholdREALTurn OFF above this
prev_outputBOOLPrevious output state (for memory)

10. Rate Limit (Analog)

Clamps the rate of change on an analog value — the output cannot change faster than the specified rate per second. Different from the discrete RATE_LIMITER which counts events.

(* Limit valve movement to 10%/sec up, 5%/sec down *)
valve_cmd := RATELIMIT(setpoint, valve_cmd, 10.0, 5.0, 50);
ParamTypeDescription
inputREALDesired value
prev_outputREALCurrent output (state)
max_rate_upREALMaximum increase per second
max_rate_downREALMaximum decrease per second
scan_time_msINTScan cycle time in milliseconds

11. Complete Example: Resilient Protocol Gateway

A gateway that reads from Modbus, publishes to MQTT, with full resilience:

PROGRAM POU_ResilientGateway
VAR
state : INT := 0;
ok : BOOL;
scan_count : DINT := 0;

(* Resilience handles *)
modbus_cb : STRING; (* Circuit breaker for Modbus *)
mqtt_rl : STRING; (* Rate limiter for MQTT publish *)
api_cache : STRING; (* Cache for expensive lookups *)
pub_throttle : STRING; (* Throttle alarm notifications *)
conn_bulkhead : STRING; (* Limit concurrent connections *)

(* Data *)
regs : ARRAY[0..3] OF INT;
temperature : REAL;
payload : STRING;
END_VAR

CASE state OF
0: (* Initialize resilience patterns *)
modbus_cb := CIRCUIT_BREAKER_CREATE(3, 2, 15);
mqtt_rl := RATE_LIMITER_CREATE(60, 60);
api_cache := CACHE_CREATE(100, 300);
pub_throttle := THROTTLE_CREATE(10000);
conn_bulkhead := BULKHEAD_CREATE(3);

ok := MB_CLIENT_CREATE('plc', '10.0.0.50', 502);
ok := MB_CLIENT_CONNECT('plc');
ok := MQTT_CLIENT_CREATE('broker', 'tcp://10.0.0.144:1883', 'gw');
ok := MQTT_CLIENT_CONNECT('broker');
state := 10;

10: (* Running *)
scan_count := scan_count + 1;

(* Read Modbus — protected by circuit breaker *)
IF CIRCUIT_BREAKER_ALLOW(modbus_cb) THEN
IF MB_CLIENT_CONNECTED('plc') THEN
regs := MB_READ_HOLDING('plc', 0, 4);
temperature := INT_TO_REAL(regs[0]) / 10.0;
CIRCUIT_BREAKER_RECORD_SUCCESS(modbus_cb);

(* Cache the reading *)
CACHE_SET(api_cache, 'temperature', temperature, 30);
ELSE
CIRCUIT_BREAKER_RECORD_FAILURE(modbus_cb);
MB_CLIENT_CONNECT('plc');
END_IF;
ELSE
(* Use cached value while circuit is open *)
temperature := CACHE_GET(api_cache, 'temperature', 0.0);
END_IF;

(* Publish to MQTT — rate limited *)
IF RATE_LIMITER_ALLOW(mqtt_rl) THEN
payload := JSON_STRINGIFY(JSON_OBJECT(
'temp', temperature,
'cb_state', CIRCUIT_BREAKER_STATE(modbus_cb)
));
MQTT_PUBLISH('broker', 'plant/gateway/data', payload);
END_IF;

(* High-temp alarm — throttled to once per 10s *)
IF temperature > 180.0 AND THROTTLE_ALLOW(pub_throttle) THEN
MQTT_PUBLISH('broker', 'plant/alarms/high_temp',
CONCAT('Temperature: ', REAL_TO_STRING(temperature)));
END_IF;
END_CASE;
END_PROGRAM

Appendix A: Quick Reference

Cache (12)

FunctionReturnsDescription
CACHE_CREATE([maxSize, ttl])STRINGCreate cache
CACHE_SET(h, key, val [, ttl])BOOLStore with optional TTL
CACHE_GET(h, key [, default])ANYRetrieve (with default)
CACHE_GET_OR_SET(h, key, val [, ttl])ANYGet or store default
CACHE_HAS(h, key)BOOLKey exists and not expired?
CACHE_DELETE(h, key)BOOLRemove key
CACHE_TTL(h, key)INTSeconds remaining
CACHE_EXPIRE(h, key, ttl)BOOLReset TTL
CACHE_SIZE(h)INTEntry count
CACHE_KEYS(h)ARRAYAll keys
CACHE_CLEANUP(h)INTRemove expired, return count
CACHE_CLEAR(h)BOOLRemove all

Circuit Breaker (7)

FunctionReturnsDescription
CIRCUIT_BREAKER_CREATE([fail, succ, timeout])STRINGCreate breaker
CIRCUIT_BREAKER_ALLOW(h)BOOLCheck if call permitted
CIRCUIT_BREAKER_RECORD_SUCCESS(h)BOOLRecord success
CIRCUIT_BREAKER_RECORD_FAILURE(h)BOOLRecord failure
CIRCUIT_BREAKER_STATE(h)STRING"closed"/"open"/"half-open"
CIRCUIT_BREAKER_STATS(h)MAPFull statistics
CIRCUIT_BREAKER_RESET(h)BOOLForce closed

Rate Limiter (4), Throttle (3), Debounce (3), Bulkhead (5)

FunctionReturnsDescription
RATE_LIMITER_CREATE(max, window_sec)STRINGDiscrete rate limiter
RATE_LIMITER_ALLOW(h)BOOLConsume quota
RATE_LIMITER_REMAINING(h)INTRemaining quota
RATE_LIMITER_RESET(h)BOOLClear counters
THROTTLE_CREATE(interval_ms)STRINGMinimum interval enforcer
THROTTLE_ALLOW(h)BOOLInterval elapsed?
THROTTLE_WAIT_TIME(h)INTMs until next allowed
DEBOUNCE_CREATE(delay_ms)STRINGInput debouncer
DEBOUNCE_CALL(h)BOOLRecord activation
DEBOUNCE_READY(h)BOOLStable after delay?
BULKHEAD_CREATE(max)STRINGConcurrency limiter
BULKHEAD_ACQUIRE(h)BOOLTake slot
BULKHEAD_RELEASE(h)BOOLRelease slot
BULKHEAD_AVAILABLE(h)INTRemaining slots
BULKHEAD_STATS(h)MAPConcurrency stats

Standalone Functions

FunctionReturnsDescription
FALLBACK(value, default)ANYDefault for falsy values
HYSTERESIS(input, low, high, prev)BOOLDead-band switch
RATELIMIT(in, prev, up, down, ms)REALAnalog rate limiter

ControlForge v1.0.535 | 35 Resilience Functions | Cache, Circuit Breaker, Rate Limiter, Throttle, Debounce, Bulkhead

© 2026 JMB Technical Services LLC. All rights reserved. Back to All Guides