Back to Blog

Real-Time Considerations in Robot Control Loops: Budgeting Jitter and Choosing Your Loop Rate

Why a control loop that runs at 1 kHz on average can still destabilize a joint, and how to put a number on how much timing error you can tolerate

A joint controller that is supposed to run at 1 kHz almost never runs at exactly 1 kHz. Every call to your control function actually fires somewhere between 0.9 ms and 1.3 ms after the last one, depending on what the operating system, the network stack, or a garbage collector decided to do in between. That variation is called jitter, and it is one of the most common real-time considerations in robot control loops that gets glossed over until a joint starts buzzing or a gripper misses a grasp by a few milliseconds. This article gives you the vocabulary, a way to measure jitter with real code, and a formula for how much jitter your control gains can actually tolerate before things go unstable.

What jitter actually is (and what it is not)

Jitter is not the same as latency. Latency is how long it takes from a sensor sample to an actuator command going out; jitter is how much that timing varies from cycle to cycle. A loop with 2 ms of constant, predictable latency is often fine. A loop with an average period of 1 ms but individual cycles ranging from 0.6 ms to 2.4 ms is the one that will bite you, because your controller was designed assuming a fixed timestep.

Concretely, define the target period as T and the actual measured period of cycle i as T_i. Jitter for that cycle is:

jitter_i = T_i - T

You care about two numbers from a run of many cycles: the maximum absolute jitter (your worst-case deadline miss) and the standard deviation of jitter (how consistently bad it is). A control loop with max jitter under 5% of T is usually fine for position control. Torque control loops running at several kHz are far less forgiving, because a missed deadline there shows up directly as a torque discontinuity at the joint.

Measuring jitter with a real timer

Do not trust a busy while loop with a fixed sleep() call to give you a fixed period; sleep() only guarantees you sleep at least that long, not exactly that long. On Linux, the standard pattern is to track an absolute wake time and sleep until it, using clock_nanosleep with TIMER_ABSTIME rather than a relative sleep:

struct timespec next;
clock_gettime(CLOCK_MONOTONIC, &next);
const long period_ns = 1000000; // 1 kHz target

while (running) {
    next.tv_nsec += period_ns;
    if (next.tv_nsec >= 1000000000L) {
        next.tv_nsec -= 1000000000L;
        next.tv_sec += 1;
    }
    clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);

    struct timespec now;
    clock_gettime(CLOCK_MONOTONIC, &now);
    long actual_period_ns = (now.tv_sec - prev.tv_sec) * 1000000000L
                           + (now.tv_nsec - prev.tv_nsec);
    long jitter_ns = actual_period_ns - period_ns;
    log_jitter(jitter_ns);
    prev = now;

    run_control_step();
}

Two mistakes will corrupt this measurement before you even get to gain tuning. First, calling anything that allocates memory, locks a mutex another thread might hold, or writes a log line synchronously inside the loop; a single malloc call can occasionally take 50 to 100 microseconds due to page faults, which is enormous next to a 1 ms budget. Second, running on a stock (non-PREEMPT_RT) Linux kernel and expecting consistent sub-millisecond timing under load; the default kernel scheduler is throughput-optimized, not latency-optimized, and will happily let a control thread wait several milliseconds behind a kernel task.

Getting from a soft loop to a real-time one

You have three practical levers, roughly in order of effort:

If you are already running ROS2, the executor and callback group choice also affects jitter directly, since a slow callback sharing a thread with your control callback will delay it; see our ROS2 executors and callback groups article for how to isolate a timing-critical callback onto its own thread.

How much jitter can your gains actually survive?

This is the part most articles skip: connecting a timing number to a control-stability number. A discrete PD controller implicitly assumes a fixed sample time T when you convert a continuous gain to its discrete form, particularly for the derivative term:

u_k = Kp * e_k + Kd * (e_k - e_k-1) / T

If the actual sample time on a given cycle is T + jitter instead of T, the effective derivative gain for that cycle becomes Kd / (T + jitter), not Kd / T. A cycle that runs short spikes your effective derivative gain, injecting noise-amplified torque; a cycle that runs long under-damps that step. As a rough, checkable rule for a well-tuned joint loop, keep worst-case jitter under about 10% of T:

max jitter_i < 0.1 * T

At 10% jitter on a 1 ms loop, your effective Kd varies by roughly plus or minus 10%, which most tuned PD or PID loops tolerate without visible symptoms. Push past 20 to 30% jitter and you will typically see one of two failure modes: a high-gain joint starts to buzz audibly at a frequency related to your jitter pattern, or a lower-gain joint feels mushy and lags target position more than the gains alone would predict. If you see either symptom and your average loop rate looks fine on paper, log jitter before you touch a single gain, because retuning a controller to compensate for bad timing just narrows your stable operating range instead of fixing the actual problem.

A practical checklist

Real-time considerations in robot control loops are ultimately a measurement problem before they are a tuning problem. A loop that looks fine on an oscilloscope trace of average rate can still be quietly eating into your stability margin every time a cycle runs long. Measure jitter directly, put a number on how much your gains can absorb, and you will spend far less time chasing phantom instability that was actually a scheduler problem all along.

Building or studying robotics?

Arcbotix publishes practical guides on robotics engineering, control systems and real-world builds. Explore more articles on the blog.