Realm Live Objects Explained: Zero-Copy Access, Auto-Updating Results, and Their Limits
Realm's live objects and lazy, zero-copy queries mean you never re-query after writes—but thread confinement, invalidation, and file growth trip up most teams. A worked Kotlin example plus the limits that matter.
20 Feb 2026, 21:23 UTC

The useful answer first
In Realm, query results and managed objects are live: they reflect committed writes automatically, without re-querying. Objects are also lazily materialized—a query returning 50,000 rows costs almost nothing until you actually read a property, because fields are read on demand from memory-mapped storage rather than copied into row objects. This is the core design decision that makes Realm fast, and it is also the source of its most common crashes.
If you internalize three rules, most Realm bugs disappear:
- Never re-query after a write; your existing results already updated.
- Never touch a Realm, its objects, or its results from a different thread than the one that opened the instance.
- Never hold a managed object past the close of its Realm (or after a UI lifecycle event that closes it).
How zero-copy, lazy access actually works
Traditional ORMs execute a query, read every matching row, and copy the data into plain objects in memory. Realm does the opposite. A query returns a lightweight handle into the database file, which is memory-mapped into your process. When you access task.name, Realm resolves that property at that moment, directly from the mapped file. Nothing is copied, and no full row is loaded up front.
Two consequences follow:
- Queries are cheap. Iterating a large result set only to check a condition or count items does not inflate memory the way an ORM would.
- Objects are always current. Because a managed object is a view onto the file rather than a snapshot, a committed write on this thread is immediately visible through any object or result you already hold. Change listeners fire with fine-grained change sets (which indices were inserted, deleted, or modified), which is exactly what list UIs need for animated updates.
A worked example (Kotlin SDK)
The following illustrates the pattern with the Kotlin Multiplatform SDK (v1.x style). Class and method names differ in the legacy realm-java SDK and in the Swift, JS, and .NET SDKs—check your SDK's release notes for exact names, since transaction and migration APIs changed between major versions.
// Model
class Task : RealmObject {
var name: String = ""
var done: Boolean = false
}
// Open (main thread or a dedicated background thread)
val config = RealmConfiguration.Builder(schema = setOf(Task::class)).build()
val realm = Realm.open(config)
// Write — all writes must be inside a transaction
realm.write {
copyToRealm(Task().apply { name = "Ship the release" })
}
// Observe — emits a fresh result set on every relevant commit
realm.query<Task>("done == false")
.asFlow()
.collect { results ->
// results.list already reflects the latest committed state;
// no re-query needed after any write
renderList(results.list)
}Run this on the thread that owns your UI-state layer (or a dedicated Realm thread). realm.write { } requires no special permissions but must not be nested inside another write on the same instance. The practical check: insert a task in one place, and confirm your collector fires and shows it without calling the query again. If you find yourself re-running queries after writes, you are fighting the framework.
Limits you will hit in production
Thread confinement
A Realm instance, its managed objects, and its results are confined to the thread that opened them. Passing any of them across threads throws "Realm accessed from incorrect thread"—the single most common Realm crash. Each thread opens its own instance (cheap, because of memory mapping). To move data across threads, freeze objects/results to get immutable snapshots, or copy them out into plain data classes. In a debug build, deliberately touch a managed object from a background thread once and confirm you get the expected exception—this validates that your threading model is what you think it is.
Invalidation after close
Live objects are only valid while their Realm is open. Accessing one after realm.close() throws. This bites in UI callbacks that outlive the screen: cancel collectors and drop references in the same lifecycle method that closes the Realm.
File size never shrinks on its own
Deleting objects frees space inside the file for reuse, but the file on disk does not shrink. To reclaim space, enable compaction (for example compactOnLaunch in SDKs that support it) or trigger a compacting copy. Verify by measuring file size before and after a bulk delete, then again after a compacted open.
Migrations are explicit
In the classic SDKs, adding a field without bumping schemaVersion and supplying a migration throws at open time. Treat schema changes like database migrations in any server stack: version bump plus migration block, tested against a copy of a real user file.
Common mistakes, condensed
| Mistake | Why it hurts | Fix |
|---|---|---|
| Re-querying after every write | Wasted work; results were already live | Hold the results/flow and observe changes |
| Passing managed objects to background threads | Thread-violation crash | Freeze or copy to plain objects first |
| Writing outside a transaction | Exception; writes must be atomic and explicit | Wrap all mutations in write { } |
| Expecting the file to shrink after deletes | Storage grows unbounded on churn-heavy data | Enable compaction; verify by measuring file size |
| Adding a field without a migration | Crash at open for existing users | Bump schema version and test migration |
One caveat on sync
Everything above concerns local Realm behavior, which is stable and well-established. Atlas Device Sync and related MongoDB Realm services have undergone product changes, including deprecation announcements for some offerings. Verify the current status of any sync dependency directly with MongoDB before committing to an architecture that requires it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.