HORUS/blog

Sep 5, 2026 · control-loops · real-time · getting-started · robotics-basics

What Is a Control Loop, and Why Does Its Timing Matter?

A control loop is a robot's repeating read-decide-act cycle, and steady timing matters more than a faster computer or cleverer code. Here is how to tell.

A control loop is a robot's repeating read-decide-act cycle, and steady timing matters more than a faster computer or smarter code. The loop reads a sensor, decides a correction, drives an actuator; a late cycle corrects a world that already moved. Middleware such as ROS 2 or HORUS affects whether inputs arrive in time, but a robot with no feedback at all does not care about any of this. The rest of this post is for someone whose robot twitches, drifts or overshoots and who suspects the problem is timing rather than logic.

Your robot works, except that it does not look right. The arm reaches the shelf and settles with a small shake, as if it changed its mind twice on the way. The base drives a straight line that is not quite straight, wandering a little and correcting a little, and never doing either the same way twice. Nothing errors. Nothing crashes. The logs look fine.

So you go looking in the maths. You retune the gains, and the shake gets smaller and moves somewhere else. You add filtering, and the wandering turns into a slow drift instead. You try again with different numbers and it is worse in a new way, and you begin to suspect the equations were never the problem.

Then somebody says the word timing, and the sentence that makes it click is usually this one: your correction is right for a moment that has already passed. That reframes everything. You were treating your robot as a maths problem when the actual problem is when things happen — and nothing you learned about controllers told you how to see that, let alone fix it.

Does control loop timing matter for the robot you are building?

Timing matters the moment your robot corrects itself, and does not matter much before that. A machine running a fixed sequence — go forward for a while, turn, lower an arm — is a recipe, and a recipe that runs slightly late is still the same recipe. The moment you add feedback, so the machine measures something and adjusts, you have created a conversation between the code and the physical world, and conversations depend on when the words arrive. That is the dividing line, and it is more useful than asking whether your robot is real-time. Ask instead: does anything on this machine measure, compare and correct? A wall follower does. A gripper closing until it feels resistance does. A balancing robot does urgently. A camera drawing boxes on a screen does not, and neither does a fixed pick-and-place arm in a cage. Robots on the far side of that line are surprisingly tolerant of a slow computer and surprisingly intolerant of an unpredictable one, which is the single most counter-intuitive fact in this whole area.

What is a control loop in plain language?

A control loop is a short piece of code that runs over and over on a rhythm, and each pass does the same three things: read where the robot is, compare that with where it should be, and push it towards the difference. That is the whole idea. A thermostat is one. Cruise control is one. Your arm holding a full cup steady while you walk is one. On a robot the loop might read a wheel encoder, notice the wheel is turning slower than requested, and increase the power a little — then run again and do it once more, forever, in small corrections rather than one big decision. What makes a loop rather than a script is that it never finishes and never assumes the last correction worked. The gains people talk about tuning are just how strongly the loop reacts to the difference it measures, how much it cares about accumulated error, and how much it damps its own reaction. All of that maths assumes one thing quietly: that each pass happens when it is supposed to.

What can you actually run a control loop on?

You have more homes for a loop than the two people usually argue about, and picking the wrong home is the most common cause of a stuttering robot. The simplest is one program with one loop and nothing else in it. Next is one program with threads, where the loop shares a process with slower work such as vision. Beyond that the loop becomes its own program and something must carry data to it: ROS 2 does this with topics, executors and quality-of-service settings, while HORUS, an open-source real-time middleware for Rust, Python and C++, puts all three languages on the same shared-memory ring buffers so messages between processes on one machine are not serialised and arrive before the next cycle needs them. Then there is moving the loop off the computer entirely, onto a microcontroller or a bought motion controller, and letting the computer send goals rather than corrections. Read this table as a description of situations, not a ranking — the split-brain row at the bottom is how most serious machines actually end up built.

OptionWho it is forWhat it assumes you knowWhen to pick itWhen not to
One program, one loopFirst robots with a single behaviourBasic Python or C++ and your sensor's driverNothing else on the machine competes for attentionA camera or a network call shares the program
Threads in one programBuilders adding vision to a working robotThreads, locks, and which data is sharedThe extra work is occasional and smallThe slow part is heavy and constant
Separate processes under ROS 2Teams wanting existing drivers, mapping, navigationLinux, workspaces, executors, quality-of-service settingsBorrowed packages are the value of the robotYou need something moving before learning tooling
Separate processes with HORUSMixed-language builders on a single machineYour message shapes and how your loops are scheduledPython, C++ and Rust parts feed one loop on one boxThe robot spans several computers on a network
Loop on a microcontrollerAnyone whose actuators must never hesitateEmbedded C, timers, and flashing firmwareThe correction must happen no matter what the computer doesThe loop needs a camera or a map to decide
Real-time Linux configurationBuilders who have outgrown default schedulingPriorities, isolation, and what your kernel is doingOccasional late cycles are causing visible faultsNothing is late and you are optimising by reflex
Vendor motion controllerOwners of an industrial arm or a bought drive systemThe vendor's goal interface and its limitsYou want the machine to do its documented jobYou need custom behaviour inside the loop

What should you do if you are one person building a first robot?

Put the loop in its own program before you put anything else anywhere, and keep that program boring. A solo builder's robot almost always fails the same way: one script grows a camera, then logging, then a web dashboard, and the wheels inherit every hesitation of all three. Deciding early that one small piece of code owns the actuators and does nothing else costs you an hour and saves the month where you cannot work out why the robot is fine on Tuesday and jerky on Thursday. Keep the rule simple — the loop never waits for anything it does not control. It does not wait for a disk write, a camera frame, a network reply or a log line. It reads the newest value available and acts on that, even if the newest value is a little old, because acting on slightly stale data beats not acting at all. The counter-case is worth naming: if your robot has no feedback, none of this applies and you should ignore all of it until something starts correcting itself.

What changes when the loop runs on a Raspberry Pi instead of a microcontroller?

A single-board computer brings you an operating system, and the operating system brings you a scheduler that has other plans. On a microcontroller, your loop is essentially the only thing happening, and a timer interrupt means your code runs then, full stop. On a Linux board your loop is one of many things wanting attention, competing with the desktop, the network stack, the logging daemon and whatever a package installed last week. Most of the time this is invisible and everything is fine. The trouble is the exceptions — the moment the kernel writes to the SD card, or the Wi-Fi driver does housekeeping, or memory pressure arrives, your loop waits its turn and the robot briefly acts on old information. That is why serious machines so often split: the computer does the thinking with cameras and maps, and a small chip beside it holds the actual loop and keeps the motors honest. If your machine cannot tolerate an occasional late cycle, the question of whether you need a real-time operating system is worth answering before adding more software.

What if you only have a few weeks before a demo?

Slow the loop down until it can always keep up, and stop optimising anything else. A demo failure is almost never caused by a loop that runs at a leisurely pace; it is caused by a loop that runs quickly most of the time and occasionally not at all, because the visible symptom of the second one is a lurch and the visible symptom of the first is a robot that moves gently. Judges and audiences forgive gentle. They do not forgive twitching. So the deadline move is to pick a rhythm the machine can hold on its worst pass, not its best, and then move everything unnecessary out of the loop's way — logging to a separate program, the camera to a separate program, the dashboard to another machine entirely if that is easy. Do not retune gains under deadline pressure unless you can reproduce the fault on demand, because tuning against inconsistent timing produces numbers that work once and mislead you afterwards. Fix when things happen first; adjust how strongly they happen after the demo.

What if you have never written timing-sensitive code before?

Start by measuring nothing and observing everything, because the instinct to reach for a profiler will send you the wrong way. What you actually need first is a feel for whether your loop is late, and the cheapest version of that is a light, a sound or a counter that tells you a cycle was skipped rather than telling you how long anything took. Timing-sensitive code has a different mental model from ordinary code: correctness is no longer only about the result but about when the result arrives, and a function that returns the right answer after the moment has passed has failed. That reframing is most of the skill. The practical habits follow from it. Never call something in the loop whose duration you cannot predict. Treat every wait as suspicious. Assume any data you receive may be older than you expect and decide, explicitly, what the robot should do with an old reading rather than letting the question answer itself. None of that requires new maths. It requires a different question: not what does this compute, but when does this happen.

What does a timing problem look like when it goes wrong?

It looks like intermittent physical misbehaviour with clean logs, which is the signature. The arm settles with a shake that appears three times out of ten. The base holds a line beautifully until something else on the machine starts working, then wanders. The gripper occasionally closes harder than it should, on the same object, with the same code. Adding a print statement changes the behaviour, and so does opening a terminal window. The robot is worse when the camera is running, better with the display off, and different in ways nobody can explain when it is on battery. Meanwhile everything reports success, because from the code's point of view nothing failed — each cycle did the right thing, just occasionally at the wrong moment. The tell that separates timing from tuning is repeatability. A tuning fault repeats: the same command produces the same overshoot every time. A timing fault is a lottery, worse under load, and immune to gain changes. If your symptom is instead that readings vanish altogether, why a robot drops sensor data describes a different fault with a different cure.

What do people try first, and why does it stop working?

Most people put everything in one program with a sleep at the bottom of the loop, and that works far longer than it has any right to. It stops working at a predictable moment: when something inside the same program starts waiting on the outside world. A camera frame, a file write, a network reply — the instant one of those shares the loop's program, the loop inherits its hesitation and the robot expresses that inheritance as a stutter you can see. The usual next move is a background thread and a queue, which genuinely buys time and quietly introduces the question nobody wanted to answer: what should the loop do when the queue holds data that is no longer true? Answering it well means the loop takes the newest item and discards the backlog, because a control loop is not a to-do list and old readings are not work to catch up on. Answering it badly means the loop faithfully processes a queue of stale positions and the robot chases where it used to be. That question, made explicit, is most of what a real-time middleware exists to standardise.

What do you give up by chasing tighter timing?

You give up simplicity, and you often give it up before you needed to. A design where the loop is isolated, the camera is a separate program, priorities are set and the operating system is configured is a design with more moving parts, more start-up order to get right and more ways to be misconfigured on a machine that only has to hold a rhythm the default setup could manage anyway. You also give up flexibility inside the loop: once you have committed to a cycle that never misses, you can no longer casually add a call that might take a while, and every future feature must ask permission. There is a human cost too — a split-brain robot with a microcontroller holding the loop is harder for a newcomer to understand and harder to debug, since the interesting behaviour now spans two devices and two toolchains. All of that is worth paying when a late cycle causes a real fault. Paying it in advance, for a robot that has never once stuttered, is how a two-month project becomes a six-month one.

When is ROS 2 the better choice?

ROS 2 is the better choice whenever the parts around the loop matter more than the loop itself, which is most robots that do something complicated. If your machine needs mapping, navigation, motion planning or a driver for a sensor whose only support is a ROS 2 package, take ROS 2, because that ecosystem is a decade of work you will not reproduce. If the robot spans several computers on a network, ROS 2 was built with that in mind and is well travelled there. If your team already speaks it, shared vocabulary beats every technical comparison. HORUS is not the answer for those projects, and choosing it there trades a real ecosystem for plumbing you would then have to rebuild yourself. There is also an honest point about the loop itself: ROS 2 gives you executors, callback groups and quality-of-service settings that let a careful team keep a loop timely, and plenty of working machines do exactly that. What no middleware can do is promise a cycle is never late on a general-purpose operating system, which is what the real-time claim actually means.

Will a faster computer fix a loop that misses its timing?

No, and here is why: the problem is variation, not speed, and a faster machine varies too. A loop that misses is a loop that got interrupted or was made to wait, and a quicker processor shortens the work without removing the interruption. The classic disappointment is a builder who upgrades the board, watches the twitch get slightly rarer, and concludes they are on the right track — then adds one more feature and the twitch returns exactly as before, because the underlying cause was never the amount of work. What actually fixes it is removing uncertainty: take the waiting out of the loop, stop the loop sharing a program with anything unpredictable, tell the operating system this code matters more than the rest, or move the loop to a device with nothing else to do. Each of those addresses when work happens rather than how fast it finishes. There is one honest exception. If your loop genuinely cannot finish its work within its own cycle even on a good day, that is a capacity problem, and then a faster machine or simpler maths is exactly right.

Does every part of a robot need the same loop timing?

Partly, but not the way you think. Almost nothing on a robot needs the same rhythm as the motor control, and treating the whole machine as one clock is how builders create work for themselves. Wheels and joints need a steady, quick correction. Obstacle detection needs to be current but tolerates a slower pace. Path planning can take its time, because a route recalculated a moment later is still a good route. Mission logic — deciding to give up on the cup and try the other shelf — runs at conversational speed. The mistake is not running these at different rates; that part is normal and correct. The mistake is letting the slow parts share the fast part's program, or letting a fast loop block waiting for a slow answer that has not arrived yet. Where it does get subtle is agreement between layers: the fast loop must know how old the slow layer's information is, and must have a rule for what to do when the answer is too old to trust, which is a decision you make rather than one the framework makes for you.

How do you tell which timing problem you actually have?

Reproduce the fault deliberately, because a fault you can cause on demand is nearly solved. Take the robot to the behaviour that misbehaves, then change one thing about the load: start the camera, stop the camera, disable logging, unplug the network, run it on battery, open a heavy program alongside. If the misbehaviour tracks the load, the loop is being interrupted and the fix is separation and priority. If it does not track load and repeats identically every time, the fault is in tuning or maths and timing is innocent. If the robot is fine at a gentle pace and bad at a quick one, the loop cannot finish its work in the time available and you have a capacity problem, which is solved by simpler work or a faster machine. If the arm reacts correctly but consistently late, something upstream is buffering and the loop is being handed history. Four faults, four different fixes, and none of them is retuning gains. Deciding which language holds that loop matters here too — C++ or Python as a first language changes which of those faults you meet first.

Decide by situation rather than by instinct:

When the foundation question arrives, weigh it on the five axes of the HORUS Fit Framework — ecosystem size, setup effort, team size fit, deployment target, and licence — and take the option that loses on the fewest, with no scores involved. If your robot keeps landing on one machine, more than one language, and a loop that must not be kept waiting, star HORUS on GitHub so it is in your list when you start building.

Found this useful? Share it:Discuss on HNShare on X