HORUS/blog

Sep 5, 2026 · garbage-collection · python · real-time · robot-architecture

Is Garbage Collection a Problem for Robots?

Garbage collection only becomes a robot problem at the innermost control loop. Here is how to tell whether your stutter is the collector or your own code.

Garbage collection is a real problem only in a robot's innermost control loop, where Rust or C++ belongs and Python does not. Everything outside that loop tolerates a pause nobody can see, so the collector in Python costs you nothing there. The verdict flips when a collected language holds the code that stops the arm before it hits the table, where HORUS and ROS 2 both sit. The rest of this post is for a Python and machine-learning developer whose robot has started stuttering, trying to work out whether the collector is to blame.

You came from Python. Models, notebooks, a policy that works, and then a robot that mostly does what you tell it. Mostly is the word that started bothering you.

The arm reaches for the mug and gets there almost every time. Now and then it arrives a beat late, clips the rim, and everything afterwards is a recovery you never planned for. The logs show nothing wrong. Same code, same scene, different outcome, and no error anywhere to point at when somebody asks what happened.

Then someone in a thread told you the problem was garbage collection, that Python has no business anywhere near control, and that you should rewrite in C++. Someone else replied that this was nonsense and half of working robotics runs on Python. Both replies had plenty of agreement underneath them. Neither told you how to find out which one applies to your robot.

What you want to know is narrow. Is the collector the reason your robot misses, or is it a convenient villain for a problem you have not found yet, and what would you actually change if it turned out to be true?

Is garbage collection a problem for robots?

Garbage collection is a problem for robots only where a deadline is short enough that a pause you cannot see is a pause that breaks something. That describes a small and specific part of a robot: the code holding a joint in place, keeping a two-wheeled base upright, closing a gripper against a force reading, or driving a motor at a fixed rhythm. Miss a beat there and the machine physically moves wrong.

It does not describe most of what a robot runs. Perception deciding what is in a frame, a planner working out a route, a model choosing the next action, a behaviour tree stepping through a task: all of those can hesitate briefly without anything visible happening, because the layer beneath them keeps holding the machine steady while they think.

The part that unsettles people is not how long a collector pauses but when. Pauses arrive on the runtime's schedule rather than yours, which is why the failure shows up now and then instead of every run, and why it feels like the robot has moods. That unpredictability, not the total time spent collecting, is what makes it a control problem.

What is a garbage collector doing while your robot runs?

A garbage collector is a process inside your language runtime that finds memory your program has finished with and takes it back, on its own schedule rather than on yours. In a language without one, you release memory yourself, and the price of forgetting is a leak or a crash. In a language with one, you stop thinking about memory entirely, and the price is that you handed over the timing decision to somebody else.

Python does this in two layers. Most objects disappear the instant the last reference to them goes away, which is quiet and quick. The second layer exists because groups of objects can reference each other in a circle and keep each other alive with nobody using them, so a cyclic collector runs periodically, walks through what exists, and clears those out. That second layer is the one that pauses your program at a moment you did not choose.

Neither layer is a defect. Both are the trade every collected language makes on your behalf. Whether Python is fast enough for robot control covers the wider version of that trade.

What does a garbage collection pause look like on a real robot?

It looks like a robot that is correct almost every time and wrong at moments nothing in the logs explains. The arm reaches the mug and, occasionally, arrives late enough to catch the rim. The balancing base takes a correction step it did not need. The wheels drift through a turn that was fine on the previous lap. Every one of those is a command that should have been sent while the program was busy elsewhere.

The tell is the pattern rather than the event. Trouble clusters with how much data the program has been creating, so the robot gets worse during the busy part of a task and better during the quiet part. It gets worse the longer a process has been running and improves right after a restart, which is why teams end up restarting things before demos without ever writing down why.

If your robot varies run to run for reasons you cannot pin down, the collector is one candidate among several, and why a robot behaves differently every run walks through the others before you commit to a rewrite.

What are your actual options if the collector is hurting you?

There are eight honest options, and only three involve leaving Python. The cheapest is to stop creating work for the collector inside the loop: allocate your buffers once, reuse them every cycle, and avoid building new objects per frame. The next is to switch the cyclic collector off inside one narrow, carefully reviewed loop, which removes those pauses while reference counting keeps doing its job. The third is to relax the deadline, which is a legitimate answer for a robot that moves slowly and touches nothing fragile.

The rest move the deadline-carrying code into a language without a collector. You can rewrite that loop in C++ or in Rust, push it down onto a microcontroller that runs nothing else, keep the split inside ROS 2 with a compiled node for the loop and Python nodes for everything else, or use a middleware designed for the split. HORUS is the second shape, an open-source real-time robotics middleware for Rust, Python and C++ where all three share the same shared-memory ring buffers, so messages between processes on one machine are not serialised, under Apache-2.0 and validated in simulation.

How do these options compare side by side?

Read the last column first. Most readers can strike five rows immediately, and the honest argument is between the two that survive.

OptionWho it is forWhat it assumes you knowWhen to pick itWhen not to
Relax the deadlineSlow robots that touch nothing fragileWhat your machine does when a command is lateNothing breaks when a cycle slipsA person or an expensive object is within reach
Reuse buffers in the loopPython teams whose loop allocates every cycleWhere your code creates objects per frameThe loop is short and you own all of itLibraries in the loop allocate on your behalf
Disable the cyclic collector in one loopA single tight loop under reviewReference cycles and how memory growsThe loop is small, audited and short-livedThe same process runs a long-lived application
Rewrite the loop in C++Teams with existing C++ skill on handMemory ownership, build systems, debuggingThe loop is small and clearly separatedNobody on the team maintains C++ today
Rewrite the loop in RustNew code with a long life ahead of itOwnership rules and a smaller library poolYou are starting the loop from scratchThe team is learning the language under deadline
Push the loop to a microcontrollerMotor and joint control at a fixed rhythmEmbedded toolchains and a serial linkThe loop needs nothing the main board holdsThe loop needs camera or model output
ROS 2 with a compiled nodeTeams already invested in the ROS 2 ecosystemROS 2 nodes, launch files, message typesYou want the catalogue and the split togetherYour robot never leaves one board
HORUSOne-board robots mixing Python with Rust or C++Which loop is tight and how processes splitLanguages must share data on one machineYou need mapping, navigation and vendor drivers

Nothing there is a ranking. The last two columns decide it, and they decide differently for every robot.

Does any of this apply to you if you came from machine learning?

Probably not yet, because the code you have written so far does not hold a deadline. A model that classifies a frame, a policy that outputs the next action, a script that logs a run: none of those keeps a machine physically steady, and a pause inside any of them delays a decision rather than dropping a command. That is why so much working robotics is written in Python by people from exactly your background.

Where it starts to apply is the moment your Python sits directly between a sensor reading and a motor command with nothing underneath it. That happens sooner than people expect on a simple robot, because a simple robot has no separate controller board doing the steadying, so the script you wrote is the only thing in the chain.

So the honest question is not whether you use Python. It is whether anything below your Python is keeping the machine safe while your Python thinks. If the answer is yes, carry on. If the answer is no, that gap is the thing to fix, and the language is a detail inside it.

What hardware makes garbage collection pauses worse?

Small boards with little memory and few cores make collector pauses both longer and more frequent. A collector has more work to do when memory is tight, because reclaiming becomes urgent rather than leisurely, and it competes harder for cores when your camera pipeline, your model and your control loop are already sharing three or four of them. The same Python that behaved perfectly on your laptop can misbehave on the robot for that reason alone.

Cameras make it worse again, because image work in Python creates and discards large objects continuously, which is exactly the pattern that gives a collector something to do. A robot that was fine until you added vision often has a collector problem and a copying problem arriving together, which is part of why the diagnosis gets muddled.

The opposite case is worth knowing too. On a roomy board with cores to spare and a control loop pinned to its own, plenty of Python robots run for hours without anyone noticing the collector, because there was always a spare core available when the pause came.

How long does it take to move a control loop out of Python?

A few days when the loop is already small and clearly separated, and a month or more when the loop is tangled through everything else. The work is not the rewriting. A position controller is not much code in any language, and translating it is an afternoon. The work is the boundary: deciding what crosses between the compiled part and the Python part, how often, in what shape, and what happens when one side restarts and the other does not.

That boundary is where projects lose their time. Teams discover the loop was reading three values that live in Python objects nobody meant to share, or that the "loop" was actually four functions spread across files with a shared state dictionary between them. Untangling that is a design job, and it happens before any new language appears.

There is a shortcut worth trying first. Spend one day making the loop allocate nothing and see whether the misses stop. If they do, you have your answer without touching a compiler, and you can spend the month on something else.

What skill level does moving off a collected language assume?

It assumes you can reason about who owns a piece of memory and when that memory goes away, which is a genuinely different skill from writing correct Python. In Python, an object exists while anything refers to it and vanishes when nothing does, and you never write that down. In C++ you write it down and can be wrong in ways that crash the robot hours later. In Rust you write it down and the compiler refuses to build until the story holds together, which is slower to learn and far harder to get subtly wrong.

The second thing it assumes is a build step in your life. Edit, compile, deploy, repeat, instead of editing a file on the robot and rerunning. That change of rhythm annoys people from a Python background more than the language itself does.

If you are weighing which one to learn, whether Rust is worth it for robot software lays out the trade without assuming you already have an opinion.

What do you give up by moving the loop out of Python?

You give up the speed at which you currently change your mind. Editing a Python file on the robot and rerunning is a loop measured in seconds of your attention, and once a compiled component exists, every change to it goes through a build and a deployment. On a project where the behaviour is still being invented, that slowdown costs more than the pauses did.

You also give up one language for the whole team. Two languages means two sets of tooling, two debugging stories, two ways to write a test, and a boundary that somebody has to keep in their head. On a two-person team that boundary is free. On a team where one person knows the compiled half, it is a risk with a name.

And you give up some of the library shelf. The reason robotics teams reach for Python is the enormous amount of work already written in it, and a loop in another language cannot casually import any of that. Keep the boundary where the imports are not needed.

When is ROS 2 the better choice?

ROS 2 is the better choice whenever you need more than a way to move data around, which is most robots people actually build. Mapping, localisation, navigation, motion planning for an arm, a visualiser showing what the robot believes, recorded runs you can replay, drivers for the sensor you already bought: ROS 2 has all of it and HORUS has none of it. If the mixed-language split is your only concern, ROS 2 already solves it, because a compiled node and a Python node have always been able to live in one system.

ROS 2 also wins on people. You can hire someone who knows it, get an answer from somebody who hit your problem last year, and hand the project to a new engineer who used it at university.

And ROS 2 wins when the robot is distributed across machines, because shared memory ends at the edge of one computer. What real time actually means in robotics is worth reading first if the phrase is doing heavy lifting in your plans.

Does rewriting in Rust or C++ mean timing stops being a worry?

No, and here is why: removing the collector removes one source of unpredictable pauses and leaves every other source exactly where it was. A compiled program still waits on a lock another thread is holding. It still asks the operating system for memory at an awkward moment and waits while the request is served. It still gets set aside by the scheduler because something else was deemed more urgent. It still blocks on a file, a socket or a driver that decided to take its time.

Teams discover this the hard way. The loop is rewritten, the worst misses go away, and a smaller version of the same symptom remains, because the deadline was always being missed by two things and only one of them had a famous name.

The discipline that actually fixes timing is the same in every language: allocate ahead of time, avoid blocking inside the loop, give the loop its own core where you can, and measure what the loop waits on rather than guessing. A language without a collector makes that discipline possible. It does not apply it for you.

Is Python the reason your robot misses its deadlines?

Partly, but not the way you think: Python contributes through the code you wrote far more often than through the collector underneath it. A callback that decodes and converts an image every frame, a loop that rebuilds an array each cycle, a log line written to disk inside the control path, a model call that occasionally takes much longer than usual: each of those produces the same intermittent lateness people attribute to garbage collection, and none is cured by changing language.

There is also a scheduling story that has nothing to do with Python at all. Too many busy programs on too few cores produces pauses that look identical from outside, and so does a board quietly reducing its clock because it is hot. Both have been blamed on the collector by people who never checked.

So Python is often involved, and the collector is usually the smallest part of how. Find the largest source first, because the largest source is frequently something you can fix in an afternoon without leaving the language.

How do you tell whether the collector is really your problem?

Run three checks in order, and the answer usually appears before you finish. First, restart the robot and see whether the first minutes are cleaner than the tenth. A problem that grows with process age and resets on restart points at memory, and a problem that is equally bad from the first second does not.

Second, make the loop stop allocating. Preallocate every buffer, reuse every array, remove object creation from the cycle, and run again. If the misses vanish, the collector was involved and you now know how to keep it quiet without a rewrite. If nothing changes, stop suspecting the collector.

Third, watch what else the machine is doing when a miss happens. If misses line up with the camera pipeline being busy, or with a model call, or with logs being flushed, the cause is competition for cores rather than memory reclamation, and moving the loop to another language changes nothing about that competition.

Only after those three is a rewrite the reasonable next step, and by then you know which loop to rewrite.

Here is the whole decision in five lines.

The HORUS Fit Framework compresses this onto five axes you can score any option against: ecosystem size, setup effort, team size fit, deployment target, and licence. For this decision, team size fit does most of the work, because a two-language robot needs somebody who is comfortable on both sides of the boundary.

When your robot does grow a loop that cannot wait for a collector, the useful thing is having the option already on your shelf rather than starting a search under deadline. Put HORUS on that shelf now: star it so it is in your list when you start building.

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