ROS 2 Kilted to Lyrical Luth Migration (2026): What Breaks and Fixes
Every ROS 2 team knows the ritual. A new distribution lands in May, your dependency tree lights up red, a driver you rely on has no binaries yet, and someone on the team asks whether it is worth the disruption at all. The ros2 kilted to lyrical migration is the same ritual with a sharper deadline: Kilted Kaiju is a standard release that reaches end of life in November 2026, while Lyrical Luth is the new Long Term Support (LTS) line supported through May 2031. If you are on Kilted, you are on a clock, and staying put is not a neutral choice. The hop itself is smaller than Humble to Jazzy was, but it is not free — there is a real API break in message handling, a set of ros2_control deprecations that will silently change behaviour, and an operating-system jump if you want Tier 1 support.
This post is written for engineers who already run Kilted in anger and need to know precisely what changes, what breaks, and how to sequence the upgrade without stranding a fleet mid-transition.
What this covers: the release cadence context, a component-by-component breakdown of what changes from Kilted to Lyrical, a runnable step-by-step upgrade path with a breakage-to-fix table, the trade-offs and failure modes of mixed-distro operation, and a concrete recommendation on when to migrate versus wait.
Context and Background
ROS 2 ships one release every May on a fixed cadence, alternating between standard releases (roughly 18 months of support) and LTS releases (five years). Development happens continuously on the Rolling distribution, and each May the maintainers branch, harden, and stamp a named release out of Rolling. Lyrical Luth, announced on 22 May 2026, is the twelfth ROS 2 release and the LTS successor to the Jazzy Jalisco LTS line. Kilted Kaiju, released 23 May 2025, was the standard release that sat between the two LTS anchors. That lineage matters for planning: Kilted was never meant to be a long-lived base. It was the proving ground where features destined for Lyrical were shaken out, and its November 2026 end-of-life date was known the day it shipped.
The practical consequence is that Kilted teams face a forced decision in the second half of 2026. Once a distribution goes EOL, the buildfarm stops producing binary packages and security patches for it; your apt upgrades quietly stop delivering fixes, and any CVE in a core dependency becomes your problem to backport. This is different from the LTS-to-LTS cadence that many production teams prefer, where you can skip the intermediate standard release entirely. If you had jumped straight from Jazzy to Lyrical you would have avoided a hop; having adopted Kilted, you now inherit its shorter runway. For teams still weighing the earlier hops, our ROS 2 Jazzy to Kilted migration guide covers the previous step, and the official ROS 2 releases list documents every distribution’s support window.

Figure 1: The ROS 2 release lineage and support windows. Humble and Jazzy are LTS anchors; Kilted is the short-lived standard release between them; Lyrical Luth is the new LTS through May 2031, with Makoa Mata-mata due May 2027. Long description: a left-to-right flow showing Humble Hawksbill (LTS, EOL May 2027), Jazzy Jalisco (LTS, EOL May 2029), Kilted Kaiju (standard, EOL November 2026), Lyrical Luth (LTS, EOL May 2031), and the next release Makoa Mata-mata due May 2027, illustrating why Kilted users must move sooner than LTS users.
The rolling model is why breaking changes feel incremental rather than catastrophic: nothing lands in a named release that has not already spent months in Rolling. But “incremental” is not “zero”, and the changes that reach Lyrical from Kilted are concentrated in a few high-blast-radius areas. Knowing exactly where they are is the difference between a two-day upgrade and a two-week firefight.
It also helps to frame the ros2 kilted to lyrical migration against the alternative most teams never seriously cost out: doing nothing. A frozen Kilted deployment does not stay static — the world around it moves. Upstream libraries publish fixes only for supported distributions, base OS images accrue their own security updates that eventually diverge from what the frozen ROS build expects, and new hires arrive fluent in whatever distribution is current. The cost of migration is bounded and schedulable; the cost of drift is unbounded and arrives on someone else’s timetable, usually during an incident. That asymmetry is the real argument for treating the hop as planned engineering work rather than a chore to defer.
What Changes from Kilted to Lyrical
The headline for the ros2 kilted to lyrical migration is that the surface area is narrow but deep. Most of your application code compiles unchanged. The pain concentrates in four places: the message-serialization layer (a genuine C++ API break around uint8[] fields), ros2_control controller deprecations and parameter renames, the platform baseline (Ubuntu 26.04 for Tier 1), and the usual scattering of tooling defaults. The RMW story, contrary to a lot of hallway chatter, barely changes — Fast DDS remains the default, and Zenoh remains a Tier 1 option exactly as it was in Kilted.
RMW and the Zenoh Tier-1 Shift
Here is the nuance most migration checklists get wrong. Zenoh did not become Tier 1 in Lyrical — it became Tier 1 in Kilted, back in May 2025, which was the first ROS 2 release to ship rmw_zenoh_cpp as a Tier 1 middleware alongside Zenoh’s own 1.0 release. If you are coming from Kilted, the ros2 zenoh rmw tier 1 promotion is already behind you, not ahead. What Lyrical preserves is the status quo: rmw_fastrtps_cpp (Fast DDS) is still the default middleware, Zenoh is still a fully supported Tier 1 alternative you opt into, and Cyclone DDS remains available at a lower tier. The ROS release team was explicit about continuity here, framing Lyrical as continuing more than a decade of Fast DDS as the default.

Figure 2: The RMW layer in Lyrical Luth. Application code talks to the stable rmw abstraction; Fast DDS remains the default Tier 1 implementation, Zenoh is the opt-in Tier 1 alternative, and Cyclone DDS sits at Tier 2. Long description: a top-down diagram where ROS 2 nodes using rclcpp and rclpy call the rmw abstraction layer, which branches to rmw_fastrtps_cpp (default Tier 1) leading to DDS discovery and transport, rmw_zenoh_cpp (Tier 1 opt in) leading to a Zenoh router and peers, and rmw_cyclonedds_cpp (Tier 2) also using DDS.
So why does RMW appear on the breakage list at all? Because of an interaction between the new zero-copy message feature (below) and the middleware. The rosidl::Buffer zero-copy path in Lyrical is initially only wired up for rmw_fastrtps_cpp; publishers and subscribers using Zenoh cannot yet take advantage of it, with support described as coming soon. If your reason for evaluating Zenoh was large-payload throughput — camera frames, point clouds, GPU tensors — you now have to weigh Zenoh’s discovery and interoperability strengths against the fact that the newest copy-elimination win is Fast-DDS-only for the moment. For a deeper treatment of the underlying trade-offs, see our ROS 2 DDS vs Zenoh middleware comparison. The migration-relevant takeaway: do not switch RMW during your distribution upgrade. Change one variable at a time.
ros2_control Changes
If you run ros2_control, this is where you will spend most of your migration budget. The ros2_controllers migration guide for Kilted to Lyrical lists several deprecations and behaviour changes that will not stop your build but will change runtime behaviour if you ignore them.
The joint_state_broadcaster is the most consequential. Interfaces with data types other than double were removed from publishing to dynamic_joint_states; if you were surfacing integer or boolean interfaces through it, that path is gone and you need a custom controller or an alternative broadcaster. More subtly, the publish_dynamic_joint_states parameter is now deprecated and its default flipped to false. If your tooling or dashboards subscribe to /dynamic_joint_states, they will simply see nothing after the upgrade until you either set the parameter explicitly or migrate to state_interfaces_broadcaster, gpio_controllers, or pal_statistics for non-standard interfaces. This is a classic silent regression — the system comes up healthy, but a topic your monitoring depends on has gone quiet.
The forward-command controllers were consolidated. effort_controllers/JointGroupEffortController, position_controllers/JointGroupPositionController, and velocity_controllers/JointGroupVelocityController are all deprecated in favour of the generic forward_command_controller, which you configure by adding an interface_name parameter set to effort, position, or velocity respectively. The behaviour is equivalent, but your YAML controller configuration must change. The mobile-base controllers — diff_drive_controller, mecanum_drive_controller, and omni_wheel_drive_controller — dropped the tf_frame_prefix_enable parameter. Instead of disabling the prefix with tf_frame_prefix_enable:=false, you now set an empty string tf_frame_prefix:="", and to use the node namespace as the prefix you pass the tilde token tf_frame_prefix:="~". Teams running multiple robots on one ROS graph rely on exactly this prefixing to keep TF trees separate, so getting it wrong produces frame collisions that are maddening to debug.
One more ros2_control-adjacent change worth flagging: URDF gained acceleration, deceleration, and jerk limit fields under a new URDF version 1.2 schema. This is a forward-only, ABI-breaking addition — it is available from Lyrical onward and only honoured when the URDF version tag is 1.2 or higher; older URDFs silently initialise those limits to infinity. It will not break your migration, but if you author new URDFs against 1.2, they will not round-trip back to Kilted.
Package and API Deprecations
The single change most likely to break a C++ build is the rosidl::Buffer transition. To enable zero-copy data transfer to and from GPUs, all uint8[] fields in messages now map to rosidl::Buffer<uint8_t> in generated C++ code instead of std::vector<uint8_t>. Any code that treated a uint8[] member as a std::vector — calling .push_back, constructing from iterators, assigning a vector directly — will fail to compile until you adapt to the buffer type or copy through it. This touches a lot of real code: compressed images, serialized payloads, occupancy-grid data, and custom messages that carry byte blobs. It is the one change you cannot skim past. Python (rclpy) code is largely insulated, but C++ nodes that manipulate byte arrays need a deliberate pass. The upside is real: once adapted, and paired with a rosidl::BufferBackend, you can publish and subscribe to those payloads without copying data out of and back into GPU memory, which is transformative for perception pipelines.
Beyond that, Lyrical adds capability rather than removing it. rclpy gains a genuine AsyncNode that runs an asyncio event loop, letting you await inside subscription, service, and timer callbacks — a long-requested feature that replaces a lot of awkward threading. rclcpp gains the Callback Group Events Executor (EventsCBGExecutor), which the release team measured at 10–15% lower CPU than the standard single- and multi-threaded executors, selectable at container launch via a new --executor-type argument. Neither is a breaking change, but both are strong reasons to actually exercise the new distribution rather than treating the migration as a pure port.
Build and Tooling
The build toolchain — colcon, rosdep, ament — is stable across the hop; your existing colcon build invocations and package.xml dependency declarations carry over. There is no repeat of the ament or colcon churn that older upgrades occasionally forced, which keeps the CI surface of the ros2 kilted to lyrical migration mercifully small. The friction is at the platform boundary, not the tool boundary. Lyrical’s Tier 1 platforms are Ubuntu 26.04 “Resolute” (amd64 and arm64) and Windows 11 with Visual Studio 2022; RHEL 10 is Tier 2, and Ubuntu 24.04 Noble drops to Tier 3 alongside macOS, Debian Trixie, and OpenEmbedded. Kilted’s Tier 1 base was Ubuntu 24.04 Noble. So for full Tier 1 support you are not just changing a ROS distribution — you are changing the operating system underneath it. You can run Lyrical on Noble at Tier 3, which is a reasonable holding pattern, but you forfeit the daily-tested binary guarantees that Tier 1 gives you. Plan the OS upgrade as part of the migration, not as an afterthought, because it drives everything from your base container image to your CI runners.
In containerized deployments this is often the largest single line item. Your Dockerfile base changes from an Ubuntu 24.04 image with ros-kilted-* layers to a 26.04 image with ros-lyrical-* layers, and any apt package you pinned by exact version needs re-pinning against the Resolute archive. Multi-arch builds — the common amd64-plus-arm64 shape for robots that develop on x86 and deploy on Arm — need both architectures rebuilt and re-tested, since Lyrical’s arm64 Tier 1 status only guarantees the ROS packages, not your OS-level dependencies. Budget time for the image bake and the registry churn; it is invisible in the ROS changelog but very visible in your build pipeline.
Step-by-Step Upgrade Path
The safe sequence is to treat the migration as a fresh workspace build on a fresh OS, validated in parallel with your running Kilted system, rather than an in-place mutation. The workflow below assumes a source overlay on top of binary ROS packages, which is the common production shape.

Figure 3: The recommended Kilted-to-Lyrical migration workflow, sequenced so the running system stays intact until validation passes. Long description: a nine-step top-down flow — audit the Kilted workspace and pin versions, check driver and package availability on Lyrical, provision Ubuntu 26.04 Resolute, install ros-lyrical-desktop via apt, run rosdep update and install dependencies, rebuild the overlay with colcon, fix ros2_control and rosidl breaks, validate on hardware or in simulation, then cut over and decommission Kilted.
Step 1 — Audit and pin. Before touching anything, snapshot your current state. Record the exact versions you run today so you have a rollback target:
# Capture the current distro and installed ROS package versions
printenv ROS_DISTRO
apt list --installed 2>/dev/null | grep '^ros-kilted-' > kilted-manifest.txt
# List every dependency your workspace actually declares
rosdep keys --from-paths src --ignore-src | sort > declared-deps.txt
Step 2 — Verify availability. Check that every third-party package and driver you depend on has a Lyrical release. This is where migrations stall: a sensor driver or a nav stack plugin that has not been rebuilt for Lyrical will block you. Search the ROS index for each package under the lyrical distribution and note anything missing — those become your gating items.
Step 3 — Provision the OS and add the apt source. On a fresh Ubuntu 26.04 (Resolute) machine or container, install the ROS 2 apt source package and the desktop metapackage:
# Enable Ubuntu Universe and install the ros2-apt-source package
sudo apt update && sudo apt install -y software-properties-common curl
sudo add-apt-repository universe
export ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F '"tag_name"' | awk -F\" '{print $4}')
curl -L -o /tmp/ros2-apt-source.deb \
"https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo $VERSION_CODENAME)_all.deb"
sudo apt install -y /tmp/ros2-apt-source.deb
sudo apt update
sudo apt install -y ros-lyrical-desktop
Step 4 — Resolve dependencies with rosdep. Point rosdep at your source tree and let it install the Lyrical-flavoured system dependencies:
sudo rosdep init 2>/dev/null || true
rosdep update
# Resolve against the Lyrical distro explicitly
rosdep install --from-paths src --ignore-src -r -y --rosdistro lyrical
Step 5 — Build the overlay. Source the Lyrical underlay and rebuild your workspace cleanly. A clean build matters here because stale build/ and install/ trees from Kilted will carry the old rosidl generated headers:
source /opt/ros/lyrical/setup.bash
rm -rf build install log
colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release
This is the step where the rosidl::Buffer break surfaces as compiler errors on any uint8[] handling. Fix them by adapting to the buffer type before moving on; do not paper over them with copies unless you have profiled and decided the copy is acceptable.
Step 6 — Select and pin the RMW. Keep the middleware constant across the migration. If you ran Fast DDS on Kilted, stay on Fast DDS through the port:
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
# Only after the port is validated should you evaluate switching to Zenoh
# export RMW_IMPLEMENTATION=rmw_zenoh_cpp # then run: ros2 run rmw_zenoh_cpp rmw_zenohd
Step 7 — Update controller configs and validate. Apply the ros2_control parameter changes, then validate on simulation before hardware. Bring up your stack and confirm the controllers you expect are active and the TF tree is intact:
ros2 control list_controllers
ros2 topic echo /dynamic_joint_states --once # confirm it is (or is intentionally not) publishing
ros2 doctor --report
The table below is the compressed field guide to the ros2_control migration and the other high-frequency breakages.
| Area | What breaks | The fix |
|---|---|---|
| C++ messages | uint8[] fields now rosidl::Buffer<uint8_t>, not std::vector<uint8_t> |
Adapt byte-blob handling to the buffer type; install a rosidl::BufferBackend for zero-copy |
| joint_state_broadcaster | /dynamic_joint_states goes silent; non-double interfaces removed |
Set publish_dynamic_joint_states explicitly or move to state_interfaces_broadcaster |
| Forward controllers | JointGroup{Effort,Position,Velocity}Controller deprecated |
Use forward_command_controller with interface_name set to the interface |
| diff/mecanum/omni drive | tf_frame_prefix_enable removed |
Set tf_frame_prefix:="" to disable, or tf_frame_prefix:="~" for namespace |
| Platform | Ubuntu Noble drops from Tier 1 to Tier 3 | Move Tier 1 workloads to Ubuntu 26.04 Resolute |
| Zenoh RMW | Zero-copy rosidl::Buffer not yet supported on Zenoh |
Keep large-payload paths on Fast DDS for now |
Step 8 — Cut over. Only after the Lyrical workspace passes your validation suite on both simulation and representative hardware do you retire the Kilted deployment. Keep the kilted-manifest.txt and the old container image so a rollback is a redeploy, not a rebuild. For a worked example of validating a robotics stack on constrained hardware, our Jazzy on Jetson Orin warehouse robotics tutorial walks through the on-device build and test loop that transfers cleanly to Lyrical.
Trade-offs, Gotchas, and What Goes Wrong
Migrations rarely fail at the compiler. They fail at the seams — the period when part of your fleet runs Lyrical and part still runs Kilted, and the two are expected to talk.

Figure 4: The four failure modes of a mixed Kilted-and-Lyrical fleet and how each is resolved. Long description: a top-down diagram where a mixed Kilted and Lyrical fleet branches into four failure modes — RMW mismatch between Zenoh and Fast DDS causing no discovery, custom message ABI drift causing deserialization errors, the rosidl Buffer uint8 change causing type mismatch, and driver lag with no Lyrical binaries yet — resolved respectively by pinning one RMW fleet-wide, rebuilding all custom messages, and keeping affected nodes on Kilted.
The first seam is RMW consistency. Nodes only discover and talk to each other when they share a compatible middleware. A Lyrical node on Fast DDS and a Kilted node on Zenoh will not see each other at all — no error, just silence. If you are running a heterogeneous fleet during a phased rollout, pin the same RMW implementation everywhere and change distributions before you change middleware. Zenoh-to-DDS bridging exists, but introducing it in the middle of a distribution migration means debugging two moving parts at once.
The second seam is custom message ABI. ROS 2 does not guarantee wire compatibility for your own message definitions across a rebuild, and the rosidl::Buffer change makes this concrete: a uint8[] field serialized by a Kilted build and a Lyrical build can drift if you have not rebuilt both sides against the same interface package. Rebuild every node that uses a custom message, not just the ones you happened to edit. Half-migrated interface packages are the most common source of “it publishes but the subscriber sees garbage” reports.
The third seam is driver and dependency lag. This is the hardest constraint because it is outside your control. If a critical vendor driver has no Lyrical binaries and no buildable source, no amount of migration discipline creates one. The honest move is to keep the affected node on Kilted behind an RMW bridge, or to postpone the whole migration until the driver ships. Do not fork and hand-port a vendor driver under deadline pressure unless you are prepared to maintain that fork.
The fourth, quieter gotcha is the silent ros2_control defaults discussed earlier — the flipped publish_dynamic_joint_states default especially. Because the system comes up healthy, these regressions escape smoke tests and surface days later when someone notices a dashboard has been blank. Add explicit assertions for the topics and controllers your operations depend on, rather than trusting a clean bringup.
Practical Recommendations
Sequence the ros2 kilted to lyrical migration as a scheduled project with a named owner, not as background cleanup, and it goes smoothly; treat it as an afterthought and the driver-availability surprises will set the timeline for you. Migrate now if you are on Kilted and running anything you consider production. Kilted’s November 2026 EOL is close enough that starting the port in the second half of 2026 leaves a comfortable buffer; waiting past October means doing the work under pressure with no runway for the driver-availability surprises that always appear. The upgrade to an LTS line also buys you five years of quiet, which is worth a great deal in a robotics program where re-qualifying a base platform is expensive.
Wait — briefly — only if a business-critical dependency has no Lyrical release yet. In that case, freeze on Kilted, track the upstream package’s Lyrical progress, and migrate the moment it lands. Do not drift past EOL without a plan; an unsupported base is a slow-accumulating risk, not a stable one.
Keep a rollback plan that is a redeploy, not a rebuild. Snapshot the Kilted container image and package manifest before you start, and keep both until the Lyrical deployment has run clean for a full duty cycle on hardware. Rolling back should mean re-pointing your orchestration at the old image.
A tight pre-cutover checklist:
- Every dependency confirmed available for
lyricalon the ROS index. - Clean
colcon buildon Ubuntu 26.04 with zerorosidl::Bufferwarnings left unresolved. - Controller configs updated for the deprecations;
ros2 control list_controllersmatches expectation. /dynamic_joint_statesand other operational topics explicitly verified, not assumed.- RMW pinned and identical across any mixed-distro segment.
- Rollback image and manifest archived and tested.
If you are two hops back on Jazzy and considering skipping Kilted entirely, that Jazzy-to-Lyrical LTS-to-LTS jump is the cleaner path and is worth planning separately; our Humble-to-Jazzy migration guide shows the pattern for an LTS-to-LTS move.
Frequently Asked Questions
Is the ros2 kilted to lyrical migration mandatory?
It is not technically forced, but it is strongly advised. Kilted Kaiju reaches end of life in November 2026, after which the buildfarm stops producing binaries and security patches. Running an EOL distribution in production means you own every future CVE backport yourself. Because Lyrical Luth is an LTS release supported through May 2031, migrating trades a short-lived base for five years of maintained support, which is the right call for almost any deployed system.
Does Lyrical Luth make Zenoh the default RMW?
No. Fast DDS (rmw_fastrtps_cpp) remains the default middleware in Lyrical, continuing more than a decade as the ROS 2 default. Zenoh became a Tier 1 middleware back in Kilted Kaiju in 2025 and stays Tier 1 in Lyrical, but it is an opt-in choice, not the default. Additionally, the new zero-copy rosidl::Buffer path is initially only supported on Fast DDS, so keep large-payload topics on Fast DDS for now.
What is the biggest breaking change from Kilted to Lyrical?
For C++ code, it is that uint8[] message fields now generate as rosidl::Buffer<uint8_t> instead of std::vector<uint8_t>. Any code that treated those byte arrays as a standard vector will fail to compile until adapted. This enables zero-copy GPU data transfer, but it means every C++ node that handles byte blobs — compressed images, serialized payloads, custom messages — needs a deliberate review during the migration.
Do I have to upgrade to Ubuntu 26.04 to run Lyrical?
For Tier 1 support, yes. Lyrical Luth’s Tier 1 Linux platform is Ubuntu 26.04 “Resolute”, whereas Kilted’s was Ubuntu 24.04 Noble. You can run Lyrical on Noble at Tier 3, which is a reasonable interim step, but you lose the daily-tested binary guarantees. Plan the operating-system upgrade as part of the migration rather than treating it as separate, since it affects your base images and CI runners.
What ros2_control changes will bite me during migration?
Three things. The joint_state_broadcaster now defaults publish_dynamic_joint_states to false, so /dynamic_joint_states can go silent unless you set it explicitly. The JointGroupEffort, Position, and Velocity controllers are deprecated in favour of forward_command_controller with an interface_name parameter. And the drive controllers replaced tf_frame_prefix_enable with an empty tf_frame_prefix:="" or a namespace-substituting tf_frame_prefix:="~". Each is a config change, not a code change, but each changes runtime behaviour silently.
Can I run Kilted and Lyrical nodes on the same network during a phased rollout?
Yes, with care. Pin the same RMW implementation on every node — a mismatch, such as Zenoh on one and Fast DDS on the other, produces no discovery and no error. Rebuild every node that uses a custom message against the same interface package to avoid ABI drift. Treat driver-lagged nodes by keeping them on Kilted behind a bridge. Mixed-distro operation is a transitional state to minimize, not a steady state to design around.
Further Reading
- ROS 2 Jazzy to Kilted migration guide (2026)
- ROS 2 Jazzy Jalisco migration guide from Humble (2026)
- ROS 2 DDS vs Zenoh robotics middleware comparison (2026)
- ROS 2 Jazzy on Jetson Orin warehouse robotics tutorial (2026)
- Lyrical Luth release notes — official ROS 2 documentation
- ros2_control Kilted-to-Lyrical migration guide
By Riju — about