If you've shipped a Flutter app to production, you know the pain. A critical bug slips through, and suddenly you're staring at a review queue that could take anywhere from a few hours to a few days. Your users are stuck. You're helpless. Meanwhile, React Native developers are pushing hotfixes in minutes using CodePush.
Shorebird is Flutter's answer to that problem — an OTA (over-the-air) code push service that lets you deploy Dart code changes directly to your users' devices, bypassing the app store entirely. But how does it actually work? The answer requires understanding Flutter's compilation model at a fairly deep level, and it's genuinely fascinating engineering.
First, How Does Flutter Normally Run?
Every Flutter app you ship to the App Store or Play Store is a bundle of three things working together:
The Flutter Engine is a C++ runtime that handles everything below the Dart layer — the Skia/Impeller rendering pipeline, platform channel communication, event loops, and critically, the Dart VM itself. It's compiled as a native shared library and embedded in your app.
The Dart AOT Snapshot is your application code. When you run flutter build, the Dart compiler takes all your Dart source files and compiles
them ahead-of-time into native machine code specific to the target CPU architecture (arm64,
x86_64, etc.). The result is a binary snapshot — essentially a pre-compiled image of your
application's object heap and code.
Finally, your assets — fonts, images, JSON files — get bundled alongside everything else.
The AOT snapshot is what makes Flutter fast. Because all compilation happens at build time on your development machine, the Dart VM in release mode is stripped down to a bare executor — it just runs pre-compiled machine code. There's no JIT compiler, no bytecode interpreter, no VM service port. Just pure native execution.
What Happens in Debug Mode?
This is worth understanding because it illuminates exactly what Shorebird is doing. In debug mode, the model is completely different.
Rather than compiling to native machine code, Flutter compiles your Dart code to kernel bytecode — a platform-independent intermediate representation. The debug engine includes the full Dart VM with a JIT compiler and, crucially, a VM service protocol that listens on a socket.
When you hit hot reload in your IDE, it re-compiles only the changed files to fresh bytecode and sends them over that socket connection to the running VM. The VM swaps out the affected libraries in place — no restart needed. This works because the code was never native machine code to begin with; it's bytecode that the VM can reinterpret at any time.
Hot reload works in debug mode because nothing was ever truly compiled down to machine code. Shorebird's insight was figuring out how to approximate this in production, where AOT is a hard requirement.
This also explains why debug mode is notably slower — the full JIT VM is significantly heavier than the stripped AOT executor, and JIT compilation itself has overhead. You can't ship the debug runtime to production.
Shorebird's Approach: A Forked Engine
Shorebird's core insight is this: the Flutter Engine is open source. You can fork it, modify the Dart VM component, and build your own engine that apps ship with instead of the official one.
That's exactly what they did. The Shorebird engine is a fork of the Flutter engine with one significant addition to the Dart VM layer: the ability to download a patch and apply it to the running AOT snapshot, executing modified code through an interpreter rather than AOT.
The rendering pipeline, platform channels, and everything else in the engine remain identical to Flutter's official build. The change is localized entirely to the Dart VM component, which is why UI performance is completely unaffected.
The Patch Lifecycle
1. Creating a Release
When you run shorebird release, it compiles your Dart code into an AOT snapshot
— exactly as a normal release build would. But it also uploads that snapshot (or a hash of
it) to Shorebird's servers as the baseline for this release. This is your ground
truth.
2. Creating a Patch
When you fix a bug and run shorebird patch, it compiles your updated Dart code
into a brand new AOT snapshot. It then computes a binary diff between the
baseline snapshot and the new one. Only this delta gets uploaded to Shorebird's CDN. This is
why patches are small — you're not re-shipping your entire application, just the changes.
# The shorebird workflow
# Step 1: initial release to the app store
shorebird release --platform android
# Step 2 (later): push a fix without app store review
shorebird patch --platform android 3. Runtime Patch Application
On device, when your app launches, the Shorebird engine checks for available patches against the current release version. If a patch exists, it's downloaded in the background. The patch contains the binary diff that, when applied to the local baseline snapshot, reconstructs the new snapshot.
But here's the fundamental constraint: the device has no AOT compiler. It was stripped out in release mode. So the reconstructed new snapshot cannot be re-compiled to native machine code on device.
This is where the Dart interpreter in Shorebird's VM comes in. The engine identifies which code regions changed between the baseline and patched snapshot. Unchanged regions continue executing as native AOT machine code — full speed. Changed regions are routed through the interpreter.
Android vs iOS: Very Different Under the Hood
This is where things get genuinely interesting — and where most articles gloss over the most important detail. Shorebird works fundamentally differently on Android versus iOS, because Apple and Google have very different technical and policy constraints around dynamic code execution.
Android: Straightforward
On Android, Shorebird can do the clean, obvious thing. A patch simply provides a new AOT-compiled version of your Dart code. The patched snapshot is native machine code, compiled for the target architecture (arm64, arm32, x86_64). The Shorebird engine downloads it, replaces the old snapshot, and on next launch it runs at full native AOT speed — no interpreter involved whatsoever.
Google's platform policies and Android's security model allow this. You can download and execute new native code as long as it lives in specific directories and doesn't violate Play Store policies. Shorebird's implementation takes full advantage of this freedom.
iOS: Apple's Constraints Change Everything
Apple explicitly prohibits apps from downloading and executing new native machine code. This is a hard technical and policy restriction — iOS will refuse to execute unsigned, dynamically loaded native code. This is why React Native's CodePush works the way it does (JavaScript is interpreted, not native code), and it's the core challenge Shorebird had to solve for iOS.
Shorebird's solution is an entirely different compilation target for iOS patches. Rather than compiling to native machine code, patches on iOS are compiled to a modified bytecode format that Shorebird's custom Dart interpreter can execute. This is not the same as the debug-mode JIT VM — it's a purpose-built interpreter designed specifically for this scenario.
But Shorebird made two clever engineering investments to minimize how much code actually runs through the interpreter on iOS:
First, they modified the Dart compiler to take a second parameter — the previous compiled output — and generate new output that is maximally similar to the previous version at the machine code level. This means even when you change a function, the compiler tries to produce output that reuses as much of the old binary as possible.
Second, they built a custom Dart linker that can look at the previous and new programs and decide, at a per-function level, which functions can be pulled from the original AOT-compiled store binary and executed natively, versus which truly changed and must be interpreted.
The result: Shorebird reports that even on iOS, typically 98% or more of patched code still runs from the original AOT binary at native speed. Only the genuinely changed functions go through the interpreter. And since the vast majority of a typical Flutter app is Flutter framework code (which you're not changing), the real-world interpreter overhead is minimal.
| Aspect | Android | iOS |
|---|---|---|
| Patch format | Native AOT machine code | Bytecode (interpreted) |
| Execution of patched code | Full native speed | Interpreted (changed parts only) |
| Unchanged code | AOT | AOT (original binary) |
| Why the difference? | Google allows dynamic native code | Apple prohibits it — interpreter exception applies |
| Typical % running native | 100% | ~98%+ (framework code runs AOT) |
| Code obfuscation support | Supported | Not yet supported (as of early 2026) |
The Performance Question
The honest answer is nuanced. For most real-world Flutter applications, the performance impact of patched code is imperceptible. Here's why:
The vast majority of Flutter app code is orchestration — calling APIs, transforming data models, driving UI state. These operations spend most of their time waiting on network I/O, GPU rendering (which happens entirely in C++ and is unaffected), or platform channel calls to native code. The speed at which your Dart logic executes is rarely the bottleneck.
| Code Type | Example | Impact of Interpretation |
|---|---|---|
| UI / Widget logic | setState, build methods | Negligible |
| Network / API calls | http.get, Dio requests | Negligible |
| Business logic | Data transforms, validation | Mostly fine |
| Heavy computation | Image processing, parsing | Noticeable |
| Cryptography in Dart | Pure-Dart hash/encrypt | Significant |
If your hot path involves CPU-intensive Dart code — say, a pure-Dart JSON parser processing megabytes of data in a tight loop — and that code is in a patched region, you'd see a meaningful regression. The practical solution: Shorebird patches are designed for bug fixes and minor changes. If you're rewriting a performance-critical algorithm, ship a full release.
What Shorebird Cannot Patch
Understanding the limitations is just as important as understanding the capabilities. Shorebird operates exclusively in the Dart layer. This means:
Native code changes are impossible. If you need to update Kotlin, Swift, or C++ code, you need a full app store release. This includes any changes to native plugins, platform channel implementations, or Flutter engine itself.
Assets cannot be patched. Images, fonts, JSON files bundled with the app — these live outside the Dart snapshot and cannot be updated via Shorebird. If you need to update assets, you'd typically fetch them from a remote URL anyway, which doesn't require Shorebird.
App store metadata is unaffected. Screenshots, descriptions, privacy policy URLs — these live outside the app entirely.
App Store Compliance
This is the elephant in the room for anyone considering Shorebird in a production app, especially a regulated one. Apple's App Store guidelines prohibit apps from downloading and executing code — but they include an explicit interpreter exception for code run by a built-in interpreter.
Shorebird's position is that their engine qualifies under this exception, in the same way React Native's CodePush does. The Dart interpreter is part of the app binary that was reviewed and approved. The patches it runs are Dart bytecode, not arbitrary native machine code execution.
In practice, CodePush has operated on iOS for years under this same exception without issue. Shorebird operates similarly. That said, if your application operates under financial regulatory oversight, it's worth confirming with your legal/compliance team whether OTA code deployment needs to be disclosed or approved separately from your initial app submission.
The Deeper Insight
What makes Shorebird particularly elegant is that it isn't trying to re-invent hot reload for production. It's something more subtle: it turns the AOT snapshot from an immutable artifact into something patchable, and gracefully degrades the execution model only for the portions of code that actually changed.
The unchanged 95% of your app continues running at full AOT speed. Only the handful of methods you actually fixed run interpreted. And on next full release, the accumulated patches are baked back into a fresh AOT build, and the cycle starts over.
It's less like hot reload and more like a surgeon making targeted incisions — leaving everything else running at full speed while precisely updating only what needs to change.
For Flutter teams shipping production apps where downtime or a slow bug fix process has real business cost, Shorebird is a remarkably well-engineered solution to a genuinely hard problem. Understanding the mechanics — the forked engine, the binary diff, the AOT/interpreter split — also helps you use it wisely: knowing which changes are safe to patch, which deserve a full release, and what the real performance tradeoffs look like in your specific application.
Questions or corrections? The Shorebird documentation and their open-source engine repository are excellent resources for going deeper.