ROS 2 Kilted Kaiju to Lyrical Luth Migration (2026): What Breaks
Most teams do not upgrade a ROS 2 distribution because they want to; they upgrade because support is running out and the next long-term release is finally stable. The ros2 kilted to lyrical migration sits in exactly that awkward spot: Kilted Kaiju was a short-lived, non-LTS release, and Lyrical Luth (codename lyrical), released on 22 May 2026, is the release you are expected to land on and stay on until May 2031. The move is not cosmetic. It touches your middleware layer, your ros2_control hardware interfaces, and even the C++ type of every byte buffer in your custom messages.
This guide is for maintainers who own a real robot stack and cannot afford a week of red CI. It explains what actually changes, why each change was made, and the order in which to attack the upgrade so a failure at step six does not cost you steps one through five.
What this covers: the release cadence that forces this migration, the Zenoh Tier-1 middleware shift, the concrete ros2_control and message-type breakages, and a staged, rollback-friendly upgrade path with a verification checklist.
Context and Background
ROS 2 ships a new distribution every year in May, alternating between long-term-support (LTS) releases and shorter ones. Jazzy Jalisco (2024) was an LTS. Kilted Kaiju (May 2025) was a non-LTS release with roughly a year of support. Lyrical Luth (May 2026) is the release most production teams will treat as their next stable base. Because Kilted’s support window is short, “stay on Kilted” is not a strategy; you either move forward to Lyrical or you were already on Jazzy and are weighing a two-step jump.
The headline platform change actually predates Lyrical: Kilted was the first ROS 2 release to promote Eclipse Zenoh to Tier 1 middleware through rmw_zenoh_cpp. Tier 1 means it is tested, supported, and a first-class citizen alongside the DDS vendors. Lyrical carries that forward and continues hardening it. If you are still on a Fast DDS or Cyclone DDS default, the migration is a natural moment to evaluate the switch. For the middleware trade-offs specifically, see our ROS 2 DDS vs Zenoh middleware comparison, and for the broader upgrade discipline, the ROS 2 Jazzy to Humble migration guide established the same staged method this article reuses. The authoritative change list lives in the official Lyrical Luth release notes.
What Actually Changes: Middleware, Messages, and Control
The short answer: the ros2 kilted to lyrical migration breaks in three predictable places — the RMW layer if you switch to Zenoh, the C++ representation of uint8[] message fields, and several ros2_control lifecycle and controller-chain behaviors. Everything else is mostly recompilation and dependency bumps. Attack those three first and the rest follows.

Figure 1: The three change zones of the migration — the RMW/middleware layer, the message-type layer, and the ros2_control layer — each fed by the same source workspace and each with its own verification gate.
Figure 1 frames the work as three parallel tracks rather than one monolithic upgrade. Each track has a clear boundary, its own test, and its own rollback. Treating them separately is what keeps a single failure from cascading across the whole stack.
The middleware layer and Zenoh as Tier 1
Zenoh became a Tier-1 RMW in Kilted and remains so in Lyrical. You select it at runtime with RMW_IMPLEMENTATION=rmw_zenoh_cpp, and it requires a running Zenoh router (ros2 run rmw_zenoh_cpp rmw_zenohd) for discovery in most topologies. The appeal is concrete: unlike DDS, Zenoh works cleanly over Wi-Fi, 5G, and lossy or NAT-separated links without the multicast discovery storms that make large DDS graphs fragile. For multi-robot fleets and cloud-bridged deployments, that is the difference between a network that scales and one that melts at forty nodes.
The catch is that Zenoh is a different transport with different defaults. Quality-of-service mappings, discovery, and router placement all behave differently from Fast DDS. Do not couple the distro upgrade and the RMW switch in the same commit. Land Lyrical on your existing DDS first, prove parity, then flip the RMW as a separate, reversible change.
The message-type layer
Lyrical changes how variable-length byte fields are represented in generated C++ code. Fields declared as uint8[] in .msg files now map to a dedicated rosidl::Buffer<uint8_t> type in C++ instead of the previous std::vector<uint8_t>. This is a deliberate move to give the middleware zero-copy-friendly, allocator-aware buffers, but it will break any code that assumed a std::vector API on those fields.
If you memcpy into msg.data, call msg.data.reserve(), or pass msg.data to a function expecting std::vector<uint8_t>&, that code will not compile against Lyrical. Point-cloud, image, compressed-image, and custom telemetry messages are the usual casualties because they all carry large uint8[] payloads.
The ros2_control layer
ros2_control carries the most behavior changes. The on_export_state_interfaces() method is deprecated in favor of on_export_state_interfaces_list(), so custom hardware components and controllers that export interfaces need updating. Publishing to dynamic_joint_states no longer accepts non-double interface data types. And the controller_manager now deactivates the entire controller chain if any controller in that chain fails during an update cycle, rather than letting a partial chain limp along.
A Staged, Reversible Upgrade Path
The safe path is to change one variable at a time, with a green test between each. The sequence below is ordered so that the cheapest, most-contained changes come first and the network-level change comes last.

Figure 2: The recommended upgrade sequence — branch and pin, rebuild on Lyrical with the existing RMW, fix message-buffer breakage, fix ros2_control, run hardware-in-the-loop, and only then evaluate the Zenoh switch as a separate reversible step.
Figure 2 shows why ordering matters: the message-buffer and control fixes are pure code changes you can verify in simulation, while the RMW switch is a runtime and network change best isolated at the end where it can be rolled back with a single environment variable.
Step 1 — Branch, pin, and reproduce Kilted
Create an upgrade branch and pin every dependency to its current Kilted version first, so you have a byte-for-byte reproducible baseline. Capture a full ros2 doctor report and a recording of a representative ros2 bag on the live stack. That bag is your regression oracle for the rest of the migration.
Step 2 — Rebuild on Lyrical with the same RMW
Install Lyrical alongside Kilted and rebuild the workspace using your existing DDS RMW. Do not change the middleware yet. Most packages will compile; the ones that fail will be your message-buffer and ros2_control offenders, which you now have a precise list of.
Step 3 — Fix the message-buffer breakage
For every uint8[] field, migrate std::vector<uint8_t> assumptions to the rosidl::Buffer<uint8_t> API. In practice this means using the buffer’s own data() and size() accessors and its resize semantics rather than std::vector methods. Wrap any hot-path copies in a small helper so the change is localized and reviewable.
Step 4 — Fix ros2_control
Rename on_export_state_interfaces() to on_export_state_interfaces_list() in custom hardware and controllers, and adapt to the list-returning signature. Audit anything publishing to dynamic_joint_states and ensure only double interfaces are published. Most importantly, re-test controller chains: because a single failing controller now deactivates the whole chain, latent faults that used to be silent will surface. That is a feature, but it will fail loudly the first time.
Step 5 — Hardware-in-the-loop and bag replay
Replay the Step 1 bag through the Lyrical build and compare topic-by-topic against the recorded baseline. Then run on real or simulated hardware. Only after this is green should you touch the network layer.
Step 6 — Evaluate the Zenoh switch (optional, separate)
Now, and only now, set RMW_IMPLEMENTATION=rmw_zenoh_cpp, stand up rmw_zenohd, and re-run the same bag replay and hardware test. Because this is a runtime environment variable, rollback is instant: unset it and you are back on DDS. Decide based on measured latency, discovery behavior, and multi-robot scaling — not on hype.
Here is a compact view of the decisions across the three tracks:
| Change | Track | Breaks at | Fix | Rollback |
|---|---|---|---|---|
uint8[] → rosidl::Buffer<uint8_t> |
Messages | Compile time | Use buffer API, localize copies | Revert commit |
on_export_state_interfaces_list() |
Control | Compile time | Rename + adapt signature | Revert commit |
| Chain deactivation on failure | Control | Runtime | Fix underlying controller fault | Config revert |
dynamic_joint_states double-only |
Control | Runtime | Publish only double interfaces | Config revert |
| Zenoh Tier-1 RMW | Middleware | Runtime/network | Router + QoS tuning | Env var unset |
The ros2_control Changes in Depth
ros2_control deserves its own treatment because its changes are behavioral, not just syntactic, and behavioral changes are the ones that pass CI and then fail on a real robot. The interface-export rename is mechanical, but the controller-chain and message-type changes alter runtime semantics you may have quietly depended on.

Figure 4: Under Lyrical, a single controller failing its update cycle deactivates the entire chain; a watchdog must detect the loss and drive explicit fallback logic rather than assuming the remaining controllers continue.
Figure 4 shows why the controller-chain change is more than a bug fix. In earlier releases a chained controller that faulted could leave the rest of the chain running, producing a robot that was half-controlled — often worse than a clean stop. Lyrical makes the failure explicit and total, which is safer but demands that your supervisory layer actually handle a full-chain dropout.
Interface export migration
The move from on_export_state_interfaces() to on_export_state_interfaces_list() changes both the method name and the return shape. Custom hardware components and controllers that previously constructed interfaces inline now return a list, which the framework manages. The practical migration is to update the override signature, adjust how interfaces are constructed and handed back, and confirm that lifecycle transitions still export the expected interface set. Test this by enumerating exported interfaces after activation and diffing against the Kilted baseline — a silent drop of one state interface can disable a controller downstream without an obvious error.
The dynamic_joint_states constraint
Lyrical removes support for publishing non-double interface data types to dynamic_joint_states. If you exposed integer status codes, boolean flags, or enum-like values through joint-state interfaces, those publications must move to a different mechanism — a dedicated diagnostic topic or a status message — rather than being smuggled through the joint-state channel. Audit every custom interface and classify it: real continuous quantities stay, discrete status leaves.
Real-time and executor considerations
Distribution upgrades are also the right moment to re-validate real-time behavior. Controller update loops that ran within their deadline on Kilted should be re-measured on Lyrical, because underlying changes to realtime_tools and the control loop can shift jitter characteristics. Capture loop-timing histograms before and after; a change that adds a few hundred microseconds of tail latency can be invisible in functional tests yet violate a hard control deadline.
Build System, Dependencies, and CI
The mechanics of getting a clean build are where migrations quietly lose days, so treat the toolchain as a first-class part of the plan rather than an afterthought.
Start by rebuilding with colcon against the Lyrical underlay in a clean environment, not on top of a Kilted overlay, so stale artifacts do not mask real incompatibilities. Resolve rosdep keys against Lyrical explicitly; a dependency that resolved to one version on Kilted may map to a newer package with changed behavior. Keep the two distributions in separate, sourced environments so a developer never accidentally links a Lyrical package against Kilted headers.
Third-party and vendor packages are the usual long pole. Before you start, inventory every external ROS package your stack depends on and check each for a Lyrical release or an open migration issue. A single vendor driver that still assumes std::vector<uint8_t> buffers or the old interface-export method can block an otherwise finished migration, and you want to discover that in week one, not at the release gate. Where an upstream lags, decide early whether to fork-and-patch, vendor a fixed copy, or hold the migration for that subsystem.
For CI, run Kilted and Lyrical pipelines in parallel during the transition rather than cutting over. The Kilted pipeline stays green as your rollback proof while the Lyrical pipeline goes from red to green. Feed both the same recorded bag from Step 1 so the regression comparison is apples-to-apples. On Windows runners, remember that Kilted moved to Pixi and Conda as the default installation mechanism and Lyrical continues that approach; a CI job that provisioned ROS the old way needs its environment setup rewritten, which is a pipeline change independent of your source code.
Finally, gate the merge on more than compilation. Require the bag-replay comparison and a hardware-in-the-loop smoke test to pass before the Lyrical branch merges, so “it builds” never gets mistaken for “it works.” The cost of that discipline is a few extra CI minutes; the cost of skipping it is a robot that behaves subtly differently in the field.
Trade-offs, Gotchas, and What Goes Wrong
The most common failure is coupling the RMW switch with the distro upgrade. When both change at once and something breaks, you cannot tell whether the fault is in your recompiled code or in Zenoh’s different QoS and discovery. Keep them separate.

Figure 3: Coupling the distro upgrade with the middleware switch collapses two independent variables into one, so a single failure has two possible causes; decoupling them keeps each fault attributable to one change.
Figure 3 is the whole risk model in one picture: every variable you change simultaneously multiplies the size of your debugging search space.
A second gotcha is the controller-chain deactivation behavior. Teams that relied on a degraded controller quietly continuing will find the whole chain drop out. This is safer, but if your watchdog or fallback logic assumed partial operation, you must rewrite it before deploying to a real robot.
Third, Windows users should note that Kilted moved to Pixi and Conda as the default Windows installation mechanism, and Lyrical continues down that path. If your CI provisions ROS on Windows the old way, expect the environment setup to change, not just the packages.
Finally, third-party packages lag. Your own code may be Lyrical-clean while a vendor driver still assumes std::vector buffers or the old interface export method. Inventory your external dependencies early; a blocked upstream package can stall an otherwise finished migration.
Practical Recommendations
Treat the migration as three independent tracks with independent tests, and never change more than one variable between green builds. Record a representative bag on the live Kilted stack before you touch anything — it is the cheapest regression test you will ever build. Land Lyrical on your existing DDS first; make the Zenoh decision separately and on evidence.
Budget explicitly for the ros2_control behavior changes, because they surface at runtime rather than compile time and are easy to underestimate. And inventory third-party packages before you start, so an upstream that has not migrated does not ambush you at the end.
A short pre-flight checklist:
- Branch, pin to Kilted, capture a baseline bag and
ros2 doctorreport. - Rebuild on Lyrical with the current RMW; collect the compile-failure list.
- Migrate all
uint8[]fields to therosidl::Buffer<uint8_t>API. - Update
on_export_state_interfaces_list()and auditdynamic_joint_states. - Re-test every controller chain for the new whole-chain deactivation.
- Replay the baseline bag; run hardware-in-the-loop.
- Only then evaluate
rmw_zenoh_cppas a separate, env-var-reversible step.
Frequently Asked Questions
Is Lyrical Luth an LTS release?
Lyrical Luth was released on 22 May 2026 and is supported until May 2031, giving it a long support window suitable for production. Because the previous release, Kilted Kaiju, had only a short support window, most teams treat Lyrical as their next stable base rather than staying on Kilted. Always confirm the current support dates in the official ROS 2 distribution documentation before committing a fleet, as support policies can be revised.
Do I have to switch to Zenoh when I move to Lyrical?
No. Zenoh became Tier-1 middleware in Kilted and remains Tier 1 in Lyrical, but DDS implementations are still fully supported. You can complete the entire distro migration on your existing Fast DDS or Cyclone DDS and evaluate rmw_zenoh_cpp later. Keeping the two changes separate is strongly recommended so any failure is attributable to a single variable.
What is the most disruptive code change?
For most C++ codebases, the change from std::vector<uint8_t> to rosidl::Buffer<uint8_t> for uint8[] message fields is the most disruptive, because it fails at compile time across image, point-cloud, and custom telemetry messages. It is mechanical to fix but touches many call sites, so localize the buffer access behind small helpers to keep the change reviewable.
Why does a single failing controller now stop the whole chain?
In Lyrical, the controller_manager deactivates the entire controller chain if any controller in that chain fails during an update cycle. This prevents a partially failed chain from producing unsafe or inconsistent commands. If your system depended on degraded operation, you must add explicit fallback logic rather than relying on a partial chain continuing.
Can I jump straight from Jazzy to Lyrical?
Yes, but a two-step jump compounds changes. If you are on Jazzy, you can migrate directly to Lyrical, but you inherit both the Kilted-era changes (such as Zenoh Tier 1) and the Lyrical-era changes (message buffers, control updates) at once. Stage the work carefully and test between each track; the extra distance makes the disciplined, one-variable-at-a-time approach even more important.
How do I roll back if the migration fails on the robot?
Keep Kilted installed alongside Lyrical and source the appropriate environment per deployment. Code changes live on a branch you can revert; the Zenoh switch is a single environment variable you can unset. Because you recorded a baseline bag before starting, you can objectively confirm whether the rolled-back stack matches pre-migration behavior.
Further Reading
- ROS 2 DDS vs Zenoh robotics middleware comparison (2026) — the middleware trade-offs behind the Zenoh decision.
- ROS 2 Jazzy to Humble migration guide (2026) — the same staged upgrade discipline applied to an earlier jump.
- ros2_control hardware interface tutorial (2026) — background on the interfaces affected by this migration.
- Zenoh-ROS 2 bridge tutorial (2026) — connecting Zenoh and ROS 2 in mixed deployments.
- External: Official ROS 2 Lyrical Luth release notes and the ros2_control migration guide.
By Riju — about
