The PLC Scan Cycle Explained: IEC 61131-3 Task Scheduling, Determinism and Jitter (2026)
Ask ten controls engineers what a PLC does and nine will say “it runs logic very fast.” That answer hides the only property that matters: a programmable logic controller does not run logic fast, it runs logic on a schedule, and the schedule is the product. The PLC scan cycle is the repeating loop of reading inputs, executing the program and writing outputs, and almost every mysterious machine fault, from a button that sometimes does not register to a servo axis that hums at one particular speed, traces back to a misunderstanding of how that loop is timed and what it guarantees.
This matters more in 2026 than it did a decade ago. Controllers now run multi-task IEC 61131-3 projects alongside OPC UA servers, MQTT publishers, web visualisations and sometimes containers, all sharing one CPU. Soft PLCs on real-time Linux are moving from pilot lines to production. Every one of those additions steals time from the scan, and the cost shows up as jitter rather than as an error message. You will leave this article able to budget a scan with real arithmetic, choose task types and priorities deliberately, write Structured Text that behaves the same on cycle one million as on cycle one, and recognise the jitter sources that bite in the field.
What this covers: the classic scan model, process image versus direct I/O and tag consistency, the IEC 61131-3 task model (cyclic, event, freewheeling, priorities, watchdogs), worst-case scan budgeting, the main sources of scan time jitter, synchronisation with PROFINET and EtherCAT cycles, runnable Structured Text patterns, soft PLCs on PREEMPT_RT, safety scans, OPC UA interplay and the failure modes that cause overruns.
Context and Background
The scan cycle is older than the standard that now describes it. Early relay-replacement controllers in the 1970s were single-threaded boxes that read every input terminal into memory, solved the ladder rungs top to bottom, left to right, then wrote every output terminal, and repeated until power was removed. That model survived because it is simple to reason about: within one pass the world looks frozen, so a rung cannot see half of a change. Everything modern PLC task scheduling does is an elaboration of that guarantee, extended to multiple tasks, multiple rates and multiple cores.
IEC 61131-3, the programming languages part of the IEC 61131 family, formalises the vocabulary. It defines five languages (Ladder Diagram, Function Block Diagram, Structured Text, Instruction List and Sequential Function Chart) and, crucially for this article, a software model with resources, tasks, programs and function blocks. A resource is roughly a CPU; a task is a scheduling entity attached to a resource; a program is associated with a task and is the unit that actually runs. The standard says what tasks must do (be periodic or event-triggered, have priorities, be preemptive or non-preemptive per implementation) and deliberately leaves numbers to vendors. The third edition dates from 2013; an edition 4 has been published by the IEC in 2025 according to standards catalogues, and I have not verified its changes against the text, so treat any statement here about “the standard” as applying to the long-stable task model rather than to edition-specific details. The standard is available from the IEC webstore.
Vendor runtimes differ in ways that matter. CODESYS-based controllers, which include products from dozens of vendors, expose cyclic, event, freewheeling and status-triggered tasks, priorities, and an optional per-task watchdog, as the CODESYS online help on task configuration documents. Siemens, Rockwell and others implement the same ideas with different names: organisation blocks, periodic tasks, continuous tasks. If you want an open implementation to read, our piece on the OpenPLC v3 architecture and Modbus TCP walks through a runtime whose scan loop is short enough to understand in an afternoon.
Two things have changed the stakes. First, more of what runs on a controller is not control. Diagnostics, historian feeds and OPC UA servers are all consumers of the same CPU and memory bus. Second, code is increasingly generated or assisted by tools that do not understand timing; our analysis of LLM-generated IEC 61131-3 code and its verification argues that timing semantics, not syntax, are where generated code most often goes wrong. A model can write a syntactically perfect timer that is wrong because it assumes the program runs on an interval it does not.
The central thesis of this article is simple and slightly contrarian: determinism is a budget, not a feature. A PLC is not deterministic because it is a PLC. It is deterministic to the extent that the worst-case execution time of all work competing for the CPU, plus I/O and communication overhead, fits inside the period you promised, with margin. Everything below is that sentence unpacked.
The Scan Cycle as a Reference Model
The scan cycle repeats four phases: read inputs into an input image, execute the user program against the images, write the output image to the physical outputs, and perform housekeeping and communications. The scan time is the duration of one pass. In a cyclic controller the pass is padded or triggered to a fixed period; in a freewheeling controller the next pass starts immediately, so the period equals the scan time and varies with program flow.

Figure 1: The four phases of a classic PLC scan cycle and where the input and output process images sit relative to the physical I/O.
The diagram shows why the process image exists. Between the input read and the output write, the program works only on memory copies. A rung evaluated at the top of the program and a rung evaluated at the bottom see the same value of the same input, even if the field device changed state between them. That is scan-level consistency, and it is the real reason scan-based logic is easy to reason about.
Phase by phase
Input scan. The CPU, or a dedicated I/O processor, copies the state of input modules into the input process image. On a backplane controller this takes microseconds to a few milliseconds depending on module count and bus speed. On a fieldbus-based system the copy is from the most recent received frame, which may be older than the moment the scan began. That subtlety, the age of the input data, is why scan time alone does not give you reaction time.
Program execution. Each program attached to the task runs in declared order. Within a program, networks or statements execute sequentially; a variable written in line 10 is visible in line 20 within the same pass. Execution time depends on the code path taken, which makes it data dependent. A branch that runs only when an alarm is active can make an alarm scan take several times longer than a quiet scan, and that is exactly when you least want jitter.
Output scan. The output image is copied to the output modules. Outputs written many times during the program are only applied once, with the final value. This trips newcomers: pulsing an output on and off inside one scan produces nothing at the terminal. The last write wins.
Housekeeping. The runtime services communications (programming-tool connections, fieldbus diagnostics, OPC UA or Modbus servers), updates retentive memory, runs self-tests and feeds the watchdog. Whether this phase has a guaranteed time slice, runs only when the control task sleeps, or competes at some priority is vendor specific and is one of the largest sources of surprise.
Reaction time is not scan time
A common error is to quote scan time as the response time. The worst-case time between a physical input change and the corresponding output change is longer. Using illustrative numbers: suppose the input module filters for 3 ms, the scan period is 10 ms, and the output module needs 1 ms to switch. The input change can arrive just after the input image was latched, so it waits nearly a full scan (10 ms) to be read, then waits for the program to run (up to 10 ms if execution fills the period), then the output is written at the end of the pass and switches 1 ms later. A conservative worst case is filter plus two scan periods plus output delay, roughly 3 + 20 + 1 = 24 ms. Any machine function needing a faster reaction than that cannot be done by this scan; it needs a faster task, a hardware interrupt, or a technology module with its own timing.
Process image versus direct I/O
Most runtimes offer both a process image and direct access to I/O. The image is updated at defined points; direct access reaches into the module at the moment the instruction executes. Direct access has two costs. It is slower, because every access is a bus transaction, and it breaks consistency: two reads of the same input in the same scan can return different values. On some platforms it also bypasses the fieldbus master cache, making it impossible or very expensive on remote I/O.
A good rule is to use the image everywhere and reserve direct I/O for the rare instruction where sub-scan latency is a hard requirement, then document it. If you must read a fast signal inside a long program, split the program so the time-critical part runs in a faster task that has its own image update, rather than sprinkling direct reads through slow code.
Consistency across multiple tasks
Once you have more than one task, the single-pass guarantee weakens. Each task can have its own I/O image update. If Task A writes a structured variable and Task B, running at a different rate and higher priority, preempts in the middle of the write, Task B can read half of the old value and half of the new one. This is the classic torn read, and PLCs are not immune: a 32-bit value written as two 16-bit halves on a 16-bit bus, or a UDINT on a core that does not make 32-bit accesses atomic, or a struct updated field by field, are all vulnerable.
Three defences exist. Pass data between tasks through a single atomic-width variable. Use a vendor-supplied consistency mechanism, such as a data-consistency block or a task-level “consistent area”. Or order your tasks so the producer cannot be preempted by the consumer during the write, for example by making the consumer the lower priority. The third is fragile, because it depends on configuration that someone will change. We return to this in the failure-modes section.
IEC 61131-3 Task Scheduling in Practice
The direct answer to “how does PLC task scheduling work” is this: the runtime holds a list of tasks, each with a type (cyclic, event-triggered or freewheeling), a priority and, usually, a watchdog; a scheduler decides which ready task gets the CPU, and each task executes its assigned programs from start to finish before sleeping or being re-triggered. The standard leaves preemption policy to the implementation, but most industrial runtimes are priority-based and preemptive between tasks, never within a single statement.
Task types
A cyclic (periodic) task is released at a fixed interval, for example every 10 ms. The runtime measures from release to release, not from end to start, so the period stays constant regardless of how long the body runs, as long as the body finishes within the interval. If it does not, you have an overrun, covered below. Cyclic tasks are the default for control: PID loops, motion interpolation and sequencing belong here.
An event task runs when a condition occurs. CODESYS describes event triggering as edge-based, for example the rising edge of a global Boolean or an interrupt, and the same help page lists status-triggered tasks that can be coupled to the start, stop or reset of a POU. Event tasks suit work that is irregular but latency sensitive, such as capturing a high-speed registration mark. Their danger is that nothing bounds the event rate: a bouncing contact tied to an event task becomes a denial of service on your own CPU.
A freewheeling task has no cycle time of its own; it restarts as soon as it ends. Typically it runs at the lowest priority and soaks up leftover CPU, which makes it suitable for background work such as statistics, recipe handling or non-critical logic. Because its period equals its execution time plus whatever higher-priority tasks take, its scan time is wildly variable by design. Never put anything timing-dependent there, and be careful with timers: a TON counts real time, but the resolution of your logic is the period, which here is arbitrary.
Priorities and what they mean
The CODESYS documentation states that when several tasks are ready, the one with the higher priority is processed first, and that among tasks of equal priority the one queued longest runs first. That is the behaviour of a fixed-priority scheduler with first-in-first-out among equals, and it matters for how you reason about worst-case latency. A higher-priority task can delay a lower one indefinitely if its own load approaches 100 percent; that is starvation, and the only prevention is a utilisation budget.
Priority numbering conventions differ. In some systems 0 is the highest; in others it is the lowest. Do not assume. The documented range is vendor-defined, and I deliberately give no numeric range here because it varies by runtime; check the manual for your version.
A rule that survives contact with real projects: give priority by rate, not by importance. This is rate-monotonic scheduling, the classic result from Liu and Layland in 1973, which shows that assigning higher fixed priority to shorter-period tasks is optimal among fixed-priority schemes for independent periodic tasks, with a schedulable-utilisation bound that approaches about 69 percent for many tasks. In practice that means a motion task at 1 ms should outrank a sequencing task at 10 ms which should outrank a 100 ms process task, even if the process task is “more important”, because the fast task has the least slack to tolerate being delayed.
Watchdogs
A task watchdog is a supervisory timer. The runtime restarts it each time the task completes (or starts, depending on the implementation); if the timer expires first, the runtime treats it as an error. CODESYS documents that a watchdog can be enabled per task, with time and sensitivity as configuration parameters, though the specific values are for the user to set, so I give none. Sensitivity is typically the number of consecutive violations tolerated before the watchdog fires, which lets you ignore a single overrun but stop the application on a persistent one.
What happens on expiry is a policy decision with safety consequences. Many runtimes halt the application and drive outputs to a safe default; some only log. Know which yours does. A watchdog set to 100 times the nominal cycle detects almost nothing, while one set to exactly the cycle fires on harmless one-off jitter. A sensible starting point is to set the watchdog to two to three times the period with a sensitivity that tolerates one violation, then adjust using measured maximum execution time.
Execution order inside a task
Programs assigned to a task run in the order listed in the task configuration. That order is part of your design. If program Interlocks must see the outputs of program Sequence from the same scan, put it afterwards; if it must see last scan’s values, put it before, and say so in a comment. Many “intermittent” faults are one-scan-late interlocks caused by reordering two programs in a configuration tree.

Figure 2: A priority-based PLC task scheduling timeline showing a fast motion task preempting a sequence task, with a freewheeling task filling idle CPU.
Figure 2 sketches the behaviour: the motion task is released first and runs to completion, the sequence task is released at the same instant but must wait, and the freewheeling task only gets CPU in the gaps. The consequence for jitter is direct. The sequence task’s start time is its release time plus the preemption delay caused by every higher-priority task, so its jitter is bounded below by the execution-time variance of the motion task. Reduce the motion task’s variance and you reduce everyone else’s jitter.
Budgeting Worst-Case Scan Time with Worked Arithmetic
Budgeting turns “feels fast enough” into a number you can defend. The method is the standard response-time analysis for fixed-priority tasks, simplified for engineering use. Everything below is illustrative: the numbers are chosen for clarity and are not measurements from any product.
Step one: measure or bound execution time per task
For each task you need a worst-case execution time (WCET), not an average. Vendors usually expose last, average and maximum execution time per task in the online view. The maximum you observe in a lab is a lower bound on the true worst case, because the lab did not exercise every path. Provoke the heavy paths deliberately: all alarms active, recipe download in progress, OPC UA clients connected, maximum array lengths.
Suppose a mid-size controller has the following illustrative tasks:
| Task | Type | Period | Priority | Measured max exec | Utilisation |
|---|---|---|---|---|---|
| Motion | cyclic | 1 ms | highest | 0.22 ms | 22.0% |
| Control | cyclic | 5 ms | middle | 1.10 ms | 22.0% |
| Sequence | cyclic | 20 ms | lower | 4.50 ms | 22.5% |
| Background | freewheeling | none | lowest | whatever remains | remainder |
Total utilisation of the three cyclic tasks is 66.5 percent, which sits below the roughly 69 percent Liu-Layland bound for large task sets; for three tasks the exact bound is about 78 percent (n times 2 to the power 1/n minus 1, which is 0.78 for n of 3). The system passes the sufficient test with room to spare. But the sufficient test ignores I/O and communications overhead, which we have not yet charged against anyone.
Step two: charge the overhead
Overhead has two parts: per-cycle overhead (I/O image update, task switching) and background load (communications, housekeeping). Say the runtime spends 0.05 ms per task release on I/O update and context switching. At 1 kHz for the motion task that is another 5 percent of the CPU, at 200 Hz for control 1 percent, at 50 Hz for sequence 0.25 percent. Add about 6.3 percent, giving 72.8 percent. Now add a communications load that the vendor documents as consuming up to, say, 15 percent when a programming tool is connected and a historian is polling. You are at 87.8 percent, over the rate-monotonic bound, and the background freewheeling task will see only about 12 percent of the CPU. That is not necessarily a failure, since utilisation above the bound can still be schedulable, but it is no longer provable by the simple test, and you now need response-time analysis.
Step three: response-time analysis
For a task i, the worst-case response time R is its own execution time C plus interference from every higher-priority task j that can release during that window: R = C_i + sum over j of ceiling(R / T_j) times C_j, solved iteratively. For the Sequence task: start with R = 4.5 ms. Motion releases ceiling(4.5/1) = 5 times at 0.22 ms each = 1.10 ms. Control releases ceiling(4.5/5) = 1 time at 1.10 ms. New R = 4.5 + 1.10 + 1.10 = 6.70 ms. Iterate: motion ceiling(6.7/1) = 7 times = 1.54 ms; control ceiling(6.7/5) = 2 times = 2.20 ms; R = 4.5 + 1.54 + 2.20 = 8.24 ms. Again: motion 9 times = 1.98; control 2 times = 2.20; R = 8.68 ms. Again: motion 9 times (ceiling of 8.68 is 9) = 1.98; control 2 times = 2.20; R = 8.68 ms, converged.
So the Sequence task, with a 20 ms period, has a worst-case response time of 8.68 ms before charging overhead and communications. If it must complete by its deadline of 20 ms, there is 11.3 ms of slack. If overhead and communications interference add 25 percent on top of the interfering work, the figure rises, but not to a failure. The same analysis for Control gives R = 1.10 + ceiling(1.10/1) times 0.22 = 1.10 + 2 times 0.22 = 1.54 ms against a 5 ms period. This arithmetic can be written in a dozen lines of Python and run against your measured numbers whenever the project changes; treat it as a CI check, not a one-off calculation.
Step four: derive the watchdog and the alarms
With R known, set the watchdog above the worst-case response and below the point where the process is endangered. For Sequence that might be 15 ms against R of 8.68 ms and a 20 ms period. Then set a soft alarm at, say, 80 percent of the period for scan-time monitoring so operators hear about creeping load before the watchdog fires. Creep is the usual failure: every feature added in service takes a few hundred microseconds, none of them individually alarming.
Jitter versus latency versus overrun
Three terms are routinely confused. Latency is the delay from a trigger to the start of execution. Jitter is the variation in that delay, or in the period, from one cycle to the next. Overrun is when execution has not finished by the time the next release occurs. A system can have large constant latency and zero jitter, which is often fine for control because a constant delay can be compensated. Jitter cannot be compensated, because by definition you do not know it in advance. This is why motion control specifies jitter in microseconds while the scan time of the same task is in milliseconds.
Where Scan Time Jitter Comes From
Scan time jitter is the variation of the actual task period or response from its nominal value. It has a short list of root causes, and knowing the list lets you hunt them methodically rather than by intuition.
Communications processing
On many controllers the CPU services communications either between scans, in a time slice at the end of the scan, or in a lower-priority task. When a client connects, requests a large block or a programming tool uploads a project, that servicing time grows. Controllers that allocate a percentage of scan time to communications, as several well-known families do through a configurable setting, trade jitter for responsiveness: a higher percentage keeps the HMI snappy and lengthens the worst-case scan. If your runtime has such a setting, treat it as a jitter knob and record its value in your commissioning documents.
Retentive memory and file writes
Writing retentive data to flash or battery-backed RAM can take milliseconds, and flash writes can block while a sector is erased. A program that stores a recipe, a counter or an event log every scan is quietly inserting a variable-length stall. Batch retentive writes into a low-priority task, write only on change, and know the endurance rating of the medium; flash wear is a reliability problem as well as a timing one.
Large arrays, string handling and loops
Structured Text makes it easy to write a FOR loop over a ten-thousand-element array inside a fast task. The first time the loop runs against a full array, the scan time jumps. String operations are worse, because functions such as concatenation, find and format scale with string length and may allocate memory. Move bulk processing to a slower task, process arrays in slices (a fixed number of elements per scan, resuming from a stored index), and avoid dynamic memory in the control path.
Floating-point on small CPUs
On a processor with a floating-point unit, a REAL multiply costs a few cycles. On a small CPU that emulates floating-point in software, it can cost tens or hundreds of cycles, and transcendental functions such as SIN, EXP and SQRT considerably more. A PID with a few REAL operations is harmless; a kinematics routine with trigonometry in a 1 ms task may not be. Check the instruction timing table in the vendor datasheet, and where you can, use fixed-point scaling or precomputed lookup tables in fast tasks. The point is not that REAL is slow but that its cost is platform dependent and non-uniform, so your WCET measurement must come from the target hardware, not a simulator.
Cache, memory and shared resources
Even deterministic code has variable execution time on a modern CPU with caches, branch predictors and shared memory buses. A task that runs every millisecond may run warm 999 times and cold when a heavier task has flushed its working set. On multi-core soft PLCs, other cores contend for the memory bus and last-level cache, and System Management Interrupts (SMIs) in BIOS firmware can steal hundreds of microseconds invisibly to the operating system. Real-time tuning on x86 therefore includes disabling frequency scaling and deep C-states, isolating cores, and checking for SMI activity.
Garbage, logging and diagnostics
Verbose logging, trace buffers and online-change operations all take CPU in bursts. An online change, in which a modified program is downloaded to a running controller, can pause the scan for noticeable time on some platforms; do it only when the process tolerates it, and measure the pause in a test rig first.

Figure 4: The principal sources of scan time jitter, how they combine into a variable scan, and the downstream faults they cause when the budget is exceeded.
Figure 4 groups the sources by whether they come from your program (loops, arrays, floating point, retentive writes), from the runtime (communications, housekeeping, online change) or from the platform (caches, interrupts, firmware). The grouping matters because the fixes live with different people: the first with the programmer, the second with the vendor configuration, the third with the hardware or OS integrator.
Fieldbus Cycles, Synchronisation and the Age of Your Data
The scan is one clock. The fieldbus is another. Unless they are explicitly synchronised, they drift against each other, and the beat between them shows up as jitter in the data the program sees. This is the most underrated cause of “my axis is noisy even though the scan time is flat”.
Asynchronous operation
In the simplest arrangement the fieldbus master runs on its own cycle, say 4 ms, and updates a buffer. The PLC task runs at 10 ms and, at the start of each scan, copies from that buffer. Data age at the time of the copy varies between 0 and 4 ms because the two clocks have no phase relationship. The program sees input values with up to 4 ms of unpredictable staleness, and outputs written at the end of the scan wait up to another 4 ms before they go on the wire. For a slow process this is irrelevant. For a position loop that differentiates the signal to compute velocity, ±2 ms of timing noise on a sample becomes a visible velocity ripple at the beat frequency between the two clocks.
Synchronised operation
The cure is to lock the PLC task to the bus cycle. In EtherCAT this uses distributed clocks (DC): slaves share a common system time, and outputs and input sampling happen at defined instants relative to a sync event. The Wago EtherCAT documentation for CODESYS-based controllers illustrates the practical side: the master can synchronise to the first DC-capable slave’s time or, alternatively, synchronise the slaves to the master (options named DCSyncToMaster and DCSyncToMasterWithSysTime); a flag tells the application when distributed clocks are in sync, which usually takes a few seconds after start, and it is used to hold motion function blocks off until sync is established to avoid position jumps. A configurable in-sync window, with a documented default of 50 microseconds, defines how much jitter the sync output tolerates. The same documentation describes an option to send the slave frame just before the IEC task starts so that servo motion is smoother. I cite these as examples from one vendor; names and defaults differ elsewhere, so check yours.
PROFINET reaches a similar goal with Isochronous Real-Time (IRT) and, in newer implementations, TSN-based mechanisms; the principle is identical: the controller’s application cycle, the network transmission schedule and the device’s sampling instant are all aligned to a common clock. PROFINET RT without isochronous mode sends cyclic data at a configured update time, but the PLC scan is not locked to it, which puts you back in the asynchronous case.

Figure 3: A synchronised cycle in which the fieldbus master transmits shortly before the PLC task starts, so the task reads fresh inputs and its outputs ride the next frame.
Design rules for synchronised control
First, make the bus cycle an integer divisor of the task period, or equal to it. A 1 ms task on a 1 ms bus is the cleanest; a 10 ms task with a 1 ms bus means the program sees every tenth frame and its outputs reach the wire at the next frame. Second, put the bus-synchronised work in the task the runtime designates as the bus task and keep it light. Wago’s documentation notes that other tasks using slave outputs can cause additional messages and, with DC, multiple writes per cycle, which affects servo drives that synchronise to the write time; the corresponding setting restricts output messaging to the bus cycle task. Third, treat synchronisation loss as a first-class event: monitor it, alarm on it, and define the machine behaviour when it drops.
Reaction time through a fieldbus
Add the bus to the earlier reaction-time estimate. For an asynchronous 4 ms bus and 10 ms scan, the worst case grows by roughly 4 ms on the input side and 4 ms on the output side, for example 3 + 4 + 20 + 4 + 1 = 32 ms (illustrative). In a synchronised system with a 1 ms task and 1 ms bus, the worst case is roughly 2 to 3 ms regardless of filter and device delays. The synchronisation is therefore worth an order of magnitude in latency for the same hardware.
Structured Text Patterns That Respect the Scan
The language is not the problem, but a scan-based execution model changes what correct code looks like. Three habits matter: never assume a loop will wait, never assume an edge persists, and never assume that time advances inside the program body. A program body executes once per task release; everything that takes time must be written as a state that persists between releases.
Edge detection
Rising-edge detection needs memory of the previous scan. The standard library provides R_TRIG and F_TRIG function blocks, which compare the current input to the stored previous value and pulse their output for exactly one scan. Hand-rolled versions look like this and behave identically:
FUNCTION_BLOCK FbEdge
VAR_INPUT
xIn : BOOL;
END_VAR
VAR_OUTPUT
xRise : BOOL;
xFall : BOOL;
END_VAR
VAR
xPrev : BOOL;
END_VAR
xRise := xIn AND NOT xPrev;
xFall := NOT xIn AND xPrev;
xPrev := xIn;
END_FUNCTION_BLOCK
Note that xPrev is updated last, so the comparison uses last scan’s value. The pulse lasts one scan, whatever the scan time is. If a consumer in another, slower task reads xRise, it may never see it. That is a cross-task hazard, not a bug in the block: a one-scan pulse is only meaningful to code running at the same or faster rate. For cross-rate signalling use a latched flag with explicit acknowledgement.
Debounce
Mechanical contacts bounce for a few milliseconds. A debounce should require the raw input to be stable for a time before the filtered output changes. The IEC TON on-delay timer does this cleanly, and the timer works in real time rather than scan counts, so the filter time does not change when the scan changes:
FUNCTION_BLOCK FbDebounce
VAR_INPUT
xRaw : BOOL;
tOnDelay : TIME := T#20MS;
tOffDelay: TIME := T#20MS;
END_VAR
VAR_OUTPUT
xClean : BOOL;
END_VAR
VAR
tonRise : TON;
tonFall : TON;
END_VAR
tonRise(IN := xRaw, PT := tOnDelay);
tonFall(IN := NOT xRaw, PT := tOffDelay);
IF tonRise.Q THEN
xClean := TRUE;
ELSIF tonFall.Q THEN
xClean := FALSE;
END_IF
END_FUNCTION_BLOCK
The resolution of a timer is the scan period: a 20 ms timer in a 10 ms task can expire anywhere between 20 and 30 ms of wall-clock time after the input changes, and the first scan after the preset elapses is when the program notices. Choose a debounce time that is at least two scan periods, and remember the debounced input is another stage of latency in the reaction-time budget.
A bounded PID call
A PID call belongs in a fixed-period cyclic task, and the block must know its sample time. Vendor libraries typically require the cycle time as a parameter or measure it internally; if you pass a hard-coded value that does not match the task, every gain is silently wrong. Here is a minimal incremental-style controller written in ST so the dependency on the sample time is explicit:
FUNCTION_BLOCK FbPid
VAR_INPUT
rSetpoint : REAL;
rProcess : REAL;
rKp : REAL;
rTi : REAL; // integral time in seconds, 0 = off
rTd : REAL; // derivative time in seconds
rTs : REAL; // sample time in seconds, must equal task period
rOutMin : REAL := 0.0;
rOutMax : REAL := 100.0;
xEnable : BOOL;
END_VAR
VAR_OUTPUT
rOut : REAL;
END_VAR
VAR
rIntegral : REAL;
rPrevErr : REAL;
rErr : REAL;
rDeriv : REAL;
rRaw : REAL;
END_VAR
IF NOT xEnable OR rTs <= 0.0 THEN
rIntegral := 0.0;
rPrevErr := 0.0;
rOut := rOutMin;
RETURN;
END_IF
rErr := rSetpoint - rProcess;
rDeriv := (rErr - rPrevErr) / rTs;
rRaw := rKp * (rErr + rTd * rDeriv) + rIntegral;
// Anti-windup: only integrate when not saturated, or when it helps
IF (rRaw < rOutMax AND rRaw > rOutMin) OR
(rRaw >= rOutMax AND rErr < 0.0) OR
(rRaw <= rOutMin AND rErr > 0.0) THEN
IF rTi > 0.0 THEN
rIntegral := rIntegral + rKp * rErr * rTs / rTi;
END_IF
END_IF
rOut := LIMIT(rOutMin, rRaw, rOutMax);
rPrevErr := rErr;
END_FUNCTION_BLOCK
The jitter lesson sits in rTs. If the task period is nominally 10 ms but jitters between 8 and 14 ms, then dividing by a fixed 0.010 makes the derivative term wrong by up to 40 percent on a given scan and the integral term similarly wrong, producing noise proportional to jitter. Two remedies: read a high-resolution system timestamp and compute the actual elapsed time each scan, or, better, fix the jitter. Where the runtime exposes it, the last-cycle time of the current task is the right source for rTs.
Using state machines instead of waiting
Because a program body cannot block, sequences are written as state machines. The CASE statement on an integer state, advancing on conditions, is the idiomatic approach. Every state evaluates quickly and returns; a timeout is a TON; a retry is a counter. The anti-pattern is a WHILE loop waiting on an input, which cannot work: inputs only update between scans, so the loop never sees a change and trips the watchdog. A good code review catches any WHILE or REPEAT whose exit condition depends on I/O or on another task.
Soft PLCs, Multi-Core and PREEMPT_RT
A soft PLC is the PLC runtime on a general-purpose or industrial PC. The scan model is the same; the guarantees come from the operating system rather than purpose-built firmware, and that is where the budget gets difficult.
Real-time Linux
For years, real-time Linux meant carrying the PREEMPT_RT patch set out of tree. Coverage from Phoronix and others reports that PREEMPT_RT was merged into mainline for Linux 6.12 in 2024, initially for ARM64, RISC-V and x86 (including x86_64), after roughly two decades of out-of-tree maintenance. In practice that means a distribution can ship a real-time kernel build from standard sources rather than a separate patch. The Linux Foundation’s Real-Time Linux project and the kernel documentation describe the details; verify the configuration option and support status for your specific kernel version and distribution before relying on it.
PREEMPT_RT makes most of the kernel preemptible, converts spinlocks into sleeping, priority-inheriting locks and runs interrupt handlers as schedulable threads. The effect is that a high-priority real-time thread can preempt almost anything, so worst-case latency drops from many milliseconds to figures that, on tuned hardware, are commonly reported in the tens of microseconds. Those figures depend on hardware, firmware and tuning, so I do not quote a number as a guarantee; measure your own platform with cyclictest under realistic load for hours, not minutes, and record the maximum, not the average.
A tuning checklist for deterministic soft PLCs
Isolate one or more cores for the control runtime with kernel parameters such as isolcpus, nohz_full and rcu_nocbs, and pin the real-time threads and the fieldbus master to them. Run the control threads under the SCHED_FIFO policy with a priority above the IRQ threads that feed them but below critical kernel threads. Lock memory with mlockall to avoid page faults, and pre-fault the stack and heap. Disable CPU frequency scaling and deep sleep states in firmware, and check for SMI sources. Keep non-real-time services such as web servers, container engines and the historian on other cores. This is not exotic; it is the standard recipe, and every missed step appears as a latency spike weeks later.
Multi-core task allocation
Multi-core runtimes let you pin tasks to cores, which converts a priority-contention problem into a placement problem. A good split is one core for the bus task and motion, one for control and sequence, and one for everything non-deterministic. The trade-off is that tasks on different cores run truly in parallel, so the torn-read hazard from earlier becomes worse: preemption was a rare event on one core, and simultaneous access is routine on two. Use atomic types, explicit synchronisation primitives from the runtime, or message queues for cross-core data.
Virtualisation and containers
Running a soft PLC in a virtual machine adds a hypervisor scheduler between you and the hardware. Real-time hypervisors with dedicated core partitioning can deliver good determinism; a general-purpose cloud-style hypervisor will not. Containers share the host kernel, so they inherit the host’s real-time properties but also its contention: a noisy neighbour pinned to the same core ruins your jitter. If the architecture mandates containers, give the control container exclusive cores and a real-time scheduling class, and test with the neighbours running at full load.
Safety Scans, OPC UA and Other Co-Tenants
Safety PLC scan
A safety PLC runs the same cycle with additional guarantees: redundant execution, cross-comparison of results, and a safety function response time that is calculated and certified as a sum of its parts. Functional safety standards such as IEC 61508 and the machinery-specific IEC 62061 and ISO 13849 require that response times be demonstrated rather than assumed. The safety response time is usually computed as the input delay, plus the safety controller’s cycle time (often counted twice for worst-case), plus the bus or network watchdog and output delays. The principle is the same as the reaction-time arithmetic earlier, with more conservative multipliers and a legal obligation to document it. Never shorten a certified safety task or move it to a different priority without revisiting the calculation. For the verification angle on generated code, see our article on verifying LLM-generated IEC 61131-3 code for safety.
OPC UA publishing interplay
An OPC UA server embedded in the controller reads variables from the same memory the control tasks write. Two interactions matter. First, the server runs in a lower-priority thread and reads without synchronising to scan boundaries, so a client reading a composite structure can get values from two different scans. If consistency matters, expose a snapshot that a control task copies atomically at the end of its scan. Second, publish-subscribe and monitored-item sampling consume CPU in proportion to the number of items and the sampling interval; a client subscribing to ten thousand variables at 100 ms can add a continuous background load that appears as scan jitter in the lower priority tasks. Constrain sampling rates, limit the number of exposed nodes, and test with the expected number of clients. The OPC UA specifications are published by the OPC Foundation. For how OPC UA companion specifications fit into machine integration, see our comparison of umati, MTConnect and OPC UA for machine tools.
Runtime choice and portability
The same ST source on different runtimes can have different scan behaviour: communication slicing, image update points and timer semantics are implementation details. Our look at CODESYS 3 and the web-based IDE discusses the toolchain side; the practical advice is to retest timing after any runtime upgrade, because the new version may have moved a cost from one task to another.
Trade-offs, Gotchas, and What Goes Wrong
Every design choice above buys determinism with something else. Fast cyclic tasks cost CPU headroom. Synchronisation costs configuration complexity and a new failure class. Process images cost latency. Priorities cost fairness. Knowing the failure modes lets you choose which cost to pay.
Scan overrun
An overrun occurs when a cyclic task has not finished when its next release arrives. Runtimes handle this differently: some skip the missed release, some run the next cycle immediately to catch up, and some raise an exception. Catch-up behaviour is dangerous for time-based logic because two scans run back to back with a near-zero interval, which makes an integrator or a derivative see an almost zero sample time. Skip behaviour loses a sample but preserves spacing. Know which one your runtime uses, and make the control block robust to either by measuring the real elapsed time.
Watchdog trips
The two typical causes of a watchdog trip are a legitimately slow path (a first-run initialisation, a recipe download, an array sweep) and a genuine hang, such as a loop whose exit condition never becomes true. The distinction matters because the remedy differs: slow paths need work spread across scans, hangs need a code fix. Log the task name, last and maximum execution time and the program counter or last-executed program at the moment of the trip; without that, diagnosing a watchdog fault in the field is guesswork. Setting the watchdog sensitivity above one hides isolated overruns but can also mask a gradual drift toward failure, so alarm on the soft threshold well before the trip.
Races on shared tags
Two tasks writing the same variable is a race, whether the tasks are on one core or two. Typical shapes: an HMI write and a program write to the same setpoint, a fast task and a slow task both clearing a flag, or a communications routine writing a structure while control reads it. The scan-level consistency guarantee of a single task does not extend across tasks. A discipline that works: every variable has exactly one writer. Commands from other sources go through a request variable that the single owner consumes and acknowledges. Naming conventions that encode ownership make violations visible in review.
Online change surprises
An online change that modifies variable layout can reinitialise or move retained data, and the transition scan can execute with partly old and partly new code. Treat online change as a risky operation: rehearse on an identical test controller, restrict it to logic changes without interface changes, and verify afterwards that scan-time statistics have not shifted.
Timer and counter misuse
Because timers measure wall-clock time but are evaluated once per scan, a timer preset shorter than the scan period behaves as if it were one scan. Counters incremented by a signal that is held for several scans without edge detection count scan numbers rather than events. Sequences that depend on a timer expiring within the same scan another event occurred are fragile for the same reason. When a machine behaves differently after a CPU upgrade, the faster scan exposing an ordering assumption is among the first things to check.
Hidden load growth
Controllers rarely fail the day they are commissioned. They fail eighteen months later when the third integrator has added a data logger, a camera interface and a cloud gateway. Without a recorded baseline for maximum task times, nobody notices the creep. Store the baseline, review it at every change, and make the scan-time alarm a real alarm that someone owns.
When not to use a PLC scan at all
Some timing requirements are simply beyond a software scan: sub-100-microsecond reactions, hard registration, high-speed counting and pulse generation. Those belong in hardware, whether technology modules, drive-based functions, FPGA logic or fast I/O with local output switching. Using the scan for them works until the day load changes, and then it fails in a way that is very hard to diagnose.
Practical Recommendations
Start from the requirement, not the platform. For every function, write the maximum allowed reaction time and the maximum allowed jitter. Then allocate it to a task with a period at most one third to one half of the reaction budget, since worst-case reaction time involves roughly two periods plus I/O delays. Assign priorities by rate. Keep the fast tasks few, small and free of loops over data of unbounded size. Put everything that is not time-critical in a slower task or a freewheeling background task, and stop it from touching shared state in an unprotected way.
Measure on the target hardware under worst-case conditions, record the maximum execution times and keep them in version control next to the project. Re-run the response-time analysis whenever a task is added or changed; it is cheap enough to automate. Set the watchdog from the measured worst case with margin, and set a softer scan-time alarm before it. If you use a fieldbus with distributed clocks or isochronous mode, synchronise the bus task, monitor the sync state and define what the machine does when sync is lost.
For soft PLCs, treat real-time tuning as part of the product: isolated cores, SCHED_FIFO, locked memory, disabled power management and a long-running latency test before release. For cross-task data, adopt the single-writer rule and use atomic-width or runtime-supplied consistent transfer.
A short checklist:
- Write reaction-time and jitter requirements per function before choosing task periods.
- Assign priorities by rate, and keep total fast-task utilisation well under 70 percent.
- Measure worst-case execution time on target hardware with all heavy paths provoked.
- Charge I/O update, communications and context switching against the budget.
- Use the process image; justify every direct I/O access in a comment.
- Give every shared variable one writer; transfer composites atomically.
- Never wait inside a program body; use state machines and TON timers.
- Synchronise task, bus and drives where motion needs it; alarm on sync loss.
- Set watchdog from measured data, alarm on soft thresholds, log context on trip.
- Retest timing after every runtime, firmware or project change.
Frequently Asked Questions
What is a PLC scan cycle?
The PLC scan cycle is the repeating sequence in which a controller reads its inputs into an input image, executes the user program against that image, writes the resulting output image to the physical outputs and performs housekeeping such as communications and diagnostics. The time for one pass is the scan time. Because the program works on images, all logic in a pass sees a consistent snapshot of the inputs, which is the core of why scan-based programming is predictable.
How long is a typical PLC scan time?
It depends on the CPU, program size and I/O, and ranges from well under a millisecond on small fast controllers to tens of milliseconds on large programs with heavy communications. There is no universal figure, so the useful number is your own measured maximum scan or task time, on your target, with all heavy paths active. Define the period you need first and then verify that the worst-case execution fits inside it with margin.
What is the difference between cyclic, event and freewheeling tasks in IEC 61131-3?
A cyclic task is released at a fixed interval, which gives constant sample spacing and suits control loops. An event task runs when a condition, such as the rising edge of a variable or an interrupt, occurs, which suits irregular but latency-sensitive work. A freewheeling task restarts as soon as it finishes and usually has the lowest priority, so its period varies and it suits background work only.
What causes scan time jitter in a PLC?
Common causes are communications servicing, retentive memory and file writes, large loops or array and string handling, software floating point on small CPUs, higher-priority task interference, online changes, and on soft PLCs caches, interrupts, firmware management modes and noisy neighbours on shared cores. Diagnose by recording per-task minimum, average and maximum execution time and correlating spikes with events such as client connections or alarms.
Should I use direct I/O instead of the process image to react faster?
Rarely. Direct I/O reads or writes the module at the moment of the instruction, which can reduce latency by up to one scan, but it costs more time per access and breaks scan consistency because two reads can differ within one pass. A better approach is to move the time-critical logic into a faster task with its own I/O update, or to use hardware that handles the function locally.
Can a Linux soft PLC be as deterministic as a hardware PLC?
It can reach the same order of determinism for many applications if the system is tuned: a PREEMPT_RT kernel, isolated and pinned cores, SCHED_FIFO threads, locked memory and power management disabled. Mainline Linux gained PREEMPT_RT support in version 6.12, according to widely reported accounts. Proof comes from measurement: run a long latency test under full load on your hardware and design to the observed maximum with margin, not to the average.
Further Reading
- Verifying LLM-generated IEC 61131-3 code: safety and verification in 2026
- OpenPLC v3 and Modbus TCP: an open-source industrial controller architecture
- CODESYS 3 and the web IDE in 2026
- umati vs MTConnect vs OPC UA for machine tools
- CODESYS online help: task configuration
- WAGO EtherCAT documentation: distributed clocks
- Phoronix: PREEMPT_RT support merged for Linux 6.12
- IEC webstore: IEC 61131-3
By Riju — about
