MuJoCo 4.0 Measured: 2,959× Better Integration, One Silent Actuator Change, and a Load Error That Never Says Why

MuJoCo 4.0.0 has 61 changelog items, 13 in breaking boxes. Run side by side, RK4 error fell 2,959× and an armature-free actuator whipped to 447 rad/s.

MuJoCo 4.0.0 landed on 6 November 2026. The release notes are unusually long: 61 numbered items across ten sections, four of which are headed Breaking changes. The announcement thread offers a short upgrade section that opens with five words that decide whether your week is fine or ruined: “Most models load unchanged.”

That sentence is true for most models. It was not true for the model I tried first, and it was not written nearly as loosely as it reads.

This is an audit by measurement, not by summary. I installed mujoco==3.17.0 and mujoco==4.0.0 in two separate Python 3.14 environments on the same Linux box, ran the same models in both, and recorded what changed. Every number below is from a run on this machine between 8 and 11 November 2026, not from a vendor benchmark.

The result is a release that is genuinely better at the thing it claims and more disruptive in a place it does not advertise.

What 4.0.0 says about itself

Claim from the announcementValue
PyPI downloads~3.6 million per month
GitHub stars“more than 16K”
Releases since 3.039
External contributors since 3.045+, ~2K commits in the past year
Speculative contactsrobot arm catches a 0.7 mm card at 2 m/s with a 16 ms step; discrete detection misses above 8 ms
Speedup from larger timesteps“6 to 9 times faster” for humanoid stair climbing and thin-object grasping
1D elastic rodsmatches Euler-Bernoulli cantilever deflection “within 0.5%” on CPU and Warp
Collision rebuild (3.x work, paid off here)~4× speedup over multicontact with libccd, 2× for large-mesh convex

Those last four are the claims I did not reproduce — they need their own benchmarks and their own hardware. What I did reproduce is the boring, load-bearing part: accuracy, load behaviour, package layout, and the two dynamics changes that will quietly alter results you thought were stable.

13 of 61 changes are in a warning box. Five more change your dynamics from outside one.

The release notes put 13 numbered items inside four Breaking changes blocks (Engine, Deformables, Compiler, Samples). The other 48 are ordinary features and fixes. That ratio looks reassuring until you check which behaviour changes sit outside the boxes.

ItemWhere it is in the notesWhat actually changes
#4 sidesite wrappingEngine, warning boxTendon path rule: half-plane test → homotopy rule
#5 RK4 orientationsEngine, outside any boxIntegration order 2 → 4 for ball/free joints
#24 flex edge dampingDeformables, outsideIntegrated explicitly under implicit/implicitfast
#46 accelerometer on static/mocap bodiesBug fixesReads 0 → reads proper gravitational acceleration
#47 saturated actuatorsBug fixesSaturated actuators stop contributing implicit derivatives
#50 pair solrefBug fixesElement-wise minimum → solmix mixing

Item 5 is the clearest formatting casualty: it says, in plain words, “This changes the simulation of models with ball or free joints that use RK4”, and it sits in a section whose warning box already closed two items earlier. If you skim only the boxes, you will miss it.

Item 47 is worse than a formatting problem. It sits in the middle of 16 bug fixes, where nothing is flagged, and it removes numerical damping from exactly the actuators that were being clamped. That is not a bug fix from the user’s point of view. It is a change of stability regime, filed under repair.

Measurement 1: RK4 really is fourth order now — 2,959× better at your default timestep

The claim is precise and checkable: the Runge-Kutta stages were being combined without the Runge-Kutta–Munthe-Kaas correction, so orientations converged at second order despite the method being RK4. Fixing that should show up as a slope change in an error-vs-timestep plot, and it does.

Setup: a free joint, a rigid body with an asymmetric inertia, no gravity, 0.5 s of simulated time, error measured as the norm of the difference in quaternion components against a reference integration at dt = 1.5625e-5. I used quaternion component differences rather than an arccos-based angle because the arccos(|dot|) metric saturates near 1e-8 and flattens the very errors you are trying to measure.

dt3.17.0 error (rad)4.0.0 error (rad)Improvement
0.013.778e-041.277e-072,959×
0.0059.445e-058.076e-0911,695×
0.00252.361e-055.077e-1046,504×
0.001255.903e-063.183e-11185,451×
0.0006251.476e-061.997e-12738,793×

Fitted convergence order: 2.0000 in 3.17.0, 3.9916 in 4.0.0. A second case, near-axis spin (ω = 0.4, 9, 0.3), gives order 1.9999 → 3.9847 and a 1,052× error reduction at dt = 0.01. The four-times-per-halving relationship between the two columns is the signature you would predict analytically, so this is not a lucky constant.

Practical consequence: at the timesteps people actually run (5–16 ms), 4.0 RK4 is now better than 3.17 RK4 at 1/32 of the step. The flip side is the one the notes do not spell out: your old trajectories are wrong, not the new ones. If you have regression tests pinned to 3.x RK4 output, they will fail, and the change is in your favour.

Item 4 changes sidesite semantics. The notes are honest about the mechanism: the old half-plane test could make the tendon jump onto the geom, and could pick the wrong side for wraps longer than 180°. The new rule is “the shortest path homotopic to this polyline in the wrapping plane with the geom removed”, and the notes admit it still has one discontinuity where an endpoint crosses the ray opposite the side site.

They also promise the migration is a no-op for the shipped example: the side sites of model/tendon_arm/arm26.xml “were moved from directly below the joints to 45 degrees below the arm, which reproduces the previous tendon lengths exactly.”

Both halves of that sentence are true, and I verified them separately.

The promise holds. I took the pre-4.0 arm26.xml into 3.17.0 and the migrated one into 4.0.0, swept both joints across 41 points, and compared all 246 tendon lengths. Maximum absolute difference: 2.2e-16. That is floating-point identical; the chord length arithmetic genuinely has not moved.

The migration is not optional, and failure is misdiagnosed. Load the old file under 4.0.0 and you do not get a warning about tendon wrapping. You get:

ValueError: Error: Lengthrange computation did not converge in actuator 1:
  eval (0.218496, 0.535486)  range (0.340326, 0.535486)

Nothing in that message mentions sidesite, wrapping, homotopy, or the release notes. The tendon wraps the wrong way, the actuator’s length-range search brackets a value it cannot reach, and the compiler reports a convergence failure in an actuator. A team hitting this would reasonably start debugging their actuator definitions, their lengthrange attributes, or their joint ranges. The word that would save an afternoon is not in the error.

The new site coordinates are (0.106, -0.106, 0) where the old were (0, -0.15, 0) — 0.15 at 45°, exactly as described. That is the entire fix: two numbers per side site.

A GitHub issue search for sidesite returns 25 results and 4.0.0 returns 16, and none of them report this lengthrange failure. Four days after release, this is an unreported failure mode rather than a known one.

Measurement 3: a “bug fix” that turns a frozen joint into 447 rad/s

Item 47 says saturated actuators “no longer contribute implicit actuator derivatives”, and adds, in the same paragraph, “models with light links and no armature may need joint armature or a smaller timestep.”

That reads like a mild performance note. It is a stability boundary. Here is one hinge, one position actuator (kp = 200), a joint actuatorfrcrange low enough that the actuator clamps, and 1.0 s of simulation. Final qpos and peak joint velocity:

Config3.17.04.0.0
kv=2000, dt=0.02, no armatureq = 0.063746, max abs qvel = 0.125q = 26.117906, max abs qvel = 447.56
kv=2000, dt=0.01, no armatureq = 0.126236q = 29.889235
kv=2000, dt=0.02, armature=0.01q = 0.063730q = 4.196720, max abs qvel = 9.78

In 3.17 the joint barely moves: the implicit derivative of the saturated actuator was acting as numerical damping, so the model sat at q ≈ 0.064 and looked like a stiff, well-behaved servo. In 4.0 that damping is gone — correctly, because it was never physical — and the same model whips at 447 rad/s. Adding armature=0.01 restores a physical inertia floor and the model behaves again.

Two things make this the most dangerous item in the release:

  1. It does not warn. No message, no flag, no deprecation shim. The model compiles, runs, and produces numbers.
  2. The failure looks like success in the old version. A model that outputs a suspiciously constant q is not a model that works — it is a model that was being held still by a numerical artifact that 4.0 corrects. Any test suite that asserted “joint stays near zero” is now enforcing the artifact.

Trajectories diverge as soon as kv ≥ 1 under implicit, implicitfast, or discrete. If your robots use position servos with force limits and no armature — most position-controlled arms do — this is the item to test before you upgrade, not after.

What 4.0 removes, and what it does not offer instead

  • The cable plugin (plugin/elasticity) and the <composite> element are gone, superseded by 1D flexcomp with <elasticity>. I loaded a third-party cable model: 3.17.0 accepts it (nq=157, 42 bodies), 4.0.0 rejects it with XML Error: Schema violation: unrecognized element.
  • Open elastic 1D flexcomps with at least two elements now grow per-edge twist bodies and hinge joints. On an 11-segment rod grid: nq 33 → 44, nbody 12 → 23, njnt 33 → 44, with 11 _twist_N joints and nplastic = 76. The notes do warn that this “shifts the indices of bodies and joints declared after the flexcomp” — that means any code indexing qpos by number is now wrong, including keyframes.
  • flexvert equality constraints are removed entirely; mjEQ_FLEXVERT survives only as a placeholder that errors if used.
  • mujoco.experimental.studio is now mujoco.studio; Python 3.10 support is dropped.

The notes give migration recipes for all of these (13 of the items carry an explicit Migration: paragraph), which is better documentation than most projects manage. What they do not offer is any compatibility flag. There is no switch to restore the half-plane sidesite test, no opt-out from the RK4 correction, no way to re-enable implicit damping on saturated actuators, and no mode that loads a <composite>. Upgrading is a one-way door, and the only tool for walking through it is your own test suite.

For the flex elastic constants (item 11), the fix is a real bug correction: 3D stretching was using twice the first Lamé parameter, 2D stretching a quarter of the shear modulus, 2D bending ¾(1-ν)D instead of the plate rigidity. Models get stiffer. I checked the migration formulas against the repo’s own remapped models and they match exactly — poisson 0.2 → 0.285714286 (2ν/(1+2ν)), young 5e4 → 53571.4286 (E(1+ν')/(1+ν)), 2D 6e6 → 3.6e6 (0.75(1-ν)E). If you have calibrated soft-body parameters, they were calibrated against the wrong equations, and the remap preserves your calibration rather than your parameters.

The package split: 72 MB → 62 MB → 29 MB, and a pip upgrade that breaks

mujoco is now three distributions: mujoco-core (bindings and C library, numpy only), mujoco-studio (Studio and Filament), and mujoco (both). Measured on this machine:

InstallVersionSizeFilesImport (median of 5)
mujoco3.17.072 MB304446 ms
mujoco (default)4.0.062 MB264450 ms
mujoco-core4.0.029 MB199346 ms

The split saves 59% of the install size for headless users and about 100 ms of import time (23%). The default package saves nothing measurable — 450 ms against 446 ms — and that is the honest way to read it: the win is for people who never wanted the renderer.

The upgrade path, though, is a trap door. pip install -U mujoco on top of a 3.17.0 install leaves site-packages/mujoco at 121 files and 38.8 MB with a broken import mujoco. The notes do explain the cause — pip removes files that moved to mujoco-core — and give the repair command, which worked exactly as documented:

pip install --force-reinstall --no-deps mujoco mujoco-core mujoco-studio

After that: 264 files, 63.2 MB, 4.0.0 imports. Yet the announcement’s own “Getting Started” block leads with pip uninstall -y mujoco “if upgrading from 3.x” — a conditional in a comment, above the command people will paste. The clean sequence is:

pip uninstall -y mujoco && pip install mujoco

mujoco-core does not ship mujoco.viewer, mujoco.mjrf, or Studio. It does ship something new: mujoco/doc/, the Sphinx sources plus 7 agent-skill markdown files, so an LLM agent with the package installed can read the manual offline instead of hallucinating mj_step signatures.

Repository health

MetricValue (2026-11-11)
Stars / watchers / forks16,908 / 141 / 1,991
Watcher ratio0.834%
Fork ratio11.78%
Releases sampled58, 466 assets
Median inter-release gap29 days (max 90)
Contributors / commits100 / 6,283
Top contributor share25.8% (1,619 commits)
Commits via copybara-github337 (5.4%)

A 0.83% watcher ratio is low for a project of this importance — people star MuJoCo the way they star a standard, not a repo they intend to track. The copybara-github entry is worth knowing: 5.4% of commits arrive through Google’s internal-to-public sync, which is why some changes appear as one large export rather than a discussion.

“It has 305 open issues” is also wrong as usually quoted: the API’s open_issues_count sums issues and pull requests. At read time it was 153 + 152; the live page on 11 November showed 157 issues and 176 pull requests.

An ordered upgrade checklist

Everything below is a check I actually ran; the order matters because each step fails differently.

  1. Reinstall cleanly, then verify the file count. pip uninstall -y mujoco && pip install mujoco, then confirm the package directory has ~264 files, not ~121.
  2. Load every model before you simulate any. If one throws Lengthrange computation did not converge, look at its sidesite elements first, not its actuators. Check that endpoints do not cross the ray opposite the side site across the joint range.
  3. Grep for removals.
    grep -rl '<composite'  models/
    grep -rl 'plugin="mujoco.elasticity'  models/
    grep -rl '<flexvert\|equality="vert"'  models/
    
  4. Audit saturated actuators. Find position/velocity actuators with a forcerange or actuatorfrcrange that actually clamps. If the joint has no armature, add one (start at 0.01) or cut the timestep, then compare before/after in both versions.
  5. Diff trajectories, not scalars. Run identical q0, v0, ctrl in 3.17 and 4.0 for a horizon long enough to expose divergence. Expect orientation drift under RK4 and stiffness drift on non-interpolated flexes; a stability change means you found item 47.
  6. Re-derive any index-based code. Anything that reads qpos/qvel/body indices by number, including keyframes, is suspect for 1D rods after the twist-joint change. Address bodies and joints by name.
  7. Pin by package, not by habit. If you cannot migrate now, pin mujoco==3.17.0 and never reference mujoco-core in a 3.x requirements file — those names do not exist before 4.0.

MuJoCo ships VERSIONING.md, and it should be the first thing anyone reads before upgrading. It explains why the version numbers look strange — since 3.5.0 the scheme is SUPERMAJOR / MAJOR / MINOR_OR_PATCH, where MAJOR means breaking changes — and then says the quiet part out loud:

“the numerical properties of physics state integration mean that despite being deterministic, numerical reproducibility across versions is almost guaranteed to not hold”

and later, flatly: “numerical reproducibility is never guaranteed.”

That is the honest frame for everything above. MuJoCo is not promising your numbers survive an upgrade; it is telling you they will not. Which makes the class of change the thing to grade: a second-order term improving to fourth order is a quality change, a flex constant being off by 4× is a bug fix, and a saturated actuator losing its implicit damping is a stability change that can flip a working controller into a 447 rad/s oscillation. Only one of those three will stop you in your tracks, and it is the one filed under “Bug fixes”.

The release notes for 4.0.0 do not link VERSIONING.md anywhere. Read it anyway.

FAQ

Is 4.0 faster for normal rigid-body simulation? The 6–9× figure in the announcement comes from larger timesteps in specific tasks (humanoid stair climbing, thin-object grasping) using speculative contacts, not from a faster solver loop. For a model that already runs at a comfortable timestep, the wins are collision-stack improvements from the 3.x line, not 4.0.

Can I keep 3.x behaviour without rewriting models? For flex elastic constants, yes — the migration formulas reproduce old behaviour with new parameters, and the repo’s own models demonstrate it. For RK4, sidesite, saturated actuators, and <composite>, no: there is no compatibility flag and no dual-mode loader.

Why does my model fail to load with a lengthrange convergence error? Check sidesite first. The tendon is wrapping the geom the other way, so the actuator’s length range cannot be bracketed. Moving each side site to the middle of the wrapped arc — the shipped arm26.xml uses exactly 45° — fixes it and preserves tendon lengths to 2.2e-16.

Do I need mujoco-core or the full package? mujoco-core if you never render: 29 MB instead of 62 MB and ~100 ms faster import. It lacks mujoco.viewer, mujoco.mjrf and Studio. mujoco remains the safe default.

Is the sensor output change in item 46 going to break my state estimator? Possibly. Accelerometers and framelinacc sensors on static and mocap bodies used to return zero and now return proper gravitational acceleration (a bug introduced in 3.3.7). If your estimator assumed zero for a static IMU in the world frame, it now sees 9.81 m/s² pointing up.

Should I upgrade now? If you use RK4 with ball or free joints, yes — the accuracy gain is large and free. If you have position-controlled arms with force limits and no armature, upgrade after running step 4 and 5 above. If you depend on <composite> or the cable plugin, you have a rewrite ahead of you, not an upgrade.

Bottom line

MuJoCo 4.0.0 is the best kind of major release: it fixes real mathematics (RK4 orientations, flex constants, tendon homotopy) and it documents the migrations honestly, item by item, with formulas. Its weakest point is structural, not technical — the notes put 13 items in warning boxes and leave five dynamics-changing items outside, one of which is a stability boundary disguised as a bug fix.

The measurement that matters most is not the 2,959×. It is q = 0.063746 becoming q = 26.117906: the same model, the same timestep, the same controller, an order of magnitude apart in behaviour, with nothing in the output to tell you that anything happened. Upgrade deliberately, and bring your own trajectories to compare against — the release will not tell you which of yours were resting on an artifact.