MMKV vs AsyncStorage vs SQLite: Picking Local Storage
MMKV vs AsyncStorage vs SQLite for React Native in 2026: speed, data shape, encryption and size limits, which to use for what, and how to migrate.
Full-Stack, Mobile & AI Engineer
"Where do I store this?" comes up on every React Native project, usually several times: a theme preference, an auth token, a cached feed, a draft post, ten thousand offline records. Most apps end up using the wrong tool for at least one of them, and it shows up as slow startups, lost data or a security finding.
The three common answers are MMKV, AsyncStorage and SQLite. They're not competitors so much as tools for different shapes of data. This guide compares MMKV vs AsyncStorage vs SQLite in 2026, gives a simple rule for each kind of data, and covers encryption and migration.
Key takeaways
- MMKV is a very fast key-value store with synchronous reads, ideal for settings, flags, small cached objects and persisted state.
- AsyncStorage is a simple asynchronous key-value store; it still works, but MMKV is faster and more convenient for most new apps.
- SQLite is a real database: use it for collections that grow, need queries, or must work offline.
- Secrets don't belong in any of them unencrypted. Tokens go in the Keychain or Keystore (for example via
expo-secure-store). - Most apps use two or three: MMKV for small state, SQLite for data, secure storage for credentials.
The comparison
| MMKV | AsyncStorage | SQLite | |
|---|---|---|---|
| Data model | Key-value | Key-value (strings) | Tables and SQL queries |
| API | Synchronous | Asynchronous (promises) | Async and sync APIs (expo-sqlite) |
| Speed | Very fast (memory-mapped, JSI) | Slower for many reads and writes | Fast for queries over large data |
| Good for | Settings, flags, small cached objects, persisted stores | Simple legacy storage | Lists, records, offline data, search |
| Querying | By key only | By key only | Filtering, sorting, joins, indexes |
| Encryption | Built-in option with a key | Not built in | Possible with SQLCipher-enabled builds |
| Expo Go | Needs a development build | Works | expo-sqlite works |
When to use MMKV
MMKV (via react-native-mmkv) stores key-value data in memory-mapped files and is accessed directly through JSI, so reads are synchronous. That makes it perfect for values you need immediately at startup without awaiting anything:
import { createMMKV } from "react-native-mmkv";
export const storage = createMMKV();
storage.set("theme", "dark");
const theme = storage.getString("theme"); // synchronous, no awaitVersion 4 is built on Nitro Modules (install react-native-nitro-modules alongside it) and requires React Native 0.76 or later; older guides that use new MMKV() refer to version 3. It's also the most common backend for persisting state libraries (for example Zustand's persist middleware) and TanStack Query's cache, because the synchronous API keeps hydration fast.
Use it for: user preferences, onboarding flags, feature flag caches, persisted UI state, small cached API responses.
Don't use it for: thousands of records you need to filter or sort, or anything you'd want to query.
When AsyncStorage is still fine
@react-native-async-storage/async-storage is the long-standing default key-value store. It's asynchronous, stores strings, and works everywhere, including Expo Go. If your app already uses it for a few small values and performance is fine, there's no urgent reason to change. For new apps, MMKV is usually the better default, and AsyncStorage remains handy when you can't add native code.
When to use SQLite
As soon as your data is a collection (workouts, messages, products, orders taken offline), you want a database:
import * as SQLite from "expo-sqlite";
const db = await SQLite.openDatabaseAsync("app.db");
await db.execAsync(`
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY NOT NULL,
started_at INTEGER NOT NULL,
duration_s INTEGER NOT NULL,
synced INTEGER NOT NULL DEFAULT 0
);
`);
const pending = await db.getAllAsync("SELECT * FROM sessions WHERE synced = 0 ORDER BY started_at");SQLite gives you indexes, transactions, filtering and sorting without loading everything into memory. Pair it with an ORM such as Drizzle for typed queries and migrations, or with WatermelonDB for very large, reactive datasets.
Secrets: a fourth category
Access tokens, refresh tokens and encryption keys should live in the operating system's secure storage (the iOS Keychain and Android Keystore), through expo-secure-store or react-native-keychain. If you need MMKV or SQLite encrypted, store their encryption key in secure storage, not in your JavaScript bundle.
Using MMKV with Zustand, Redux Persist and Expo
MMKV's synchronous API makes it a natural backend for state persistence. A small adapter connects it to Zustand's persist middleware:
import { createMMKV } from "react-native-mmkv";
import { createJSONStorage } from "zustand/middleware";
const mmkv = createMMKV();
export const zustandStorage = createJSONStorage(() => ({
getItem: (key) => mmkv.getString(key) ?? null,
setItem: (key, value) => mmkv.set(key, value),
removeItem: (key) => mmkv.remove(key),
}));
// persist((set) => ({ ... }), { name: "settings", storage: zustandStorage })Redux Persist expects promises, so wrap the same three calls in Promise.resolve(...). In Expo, install it with npx expo install react-native-mmkv react-native-nitro-modules; it contains native code, so it runs in a development build rather than Expo Go.
MMKV vs expo-secure-store: they solve different problems. Secure storage keeps small secrets such as tokens in the Keychain or Keystore. MMKV is fast general storage for app data. Use both, and if MMKV holds anything sensitive, encrypt it with a key that lives in secure storage.
A simple decision rule
| Data | Store it in |
|---|---|
| Theme, language, flags, small UI state | MMKV |
| Persisted Zustand or Redux state | MMKV |
| Cached API responses for instant startup | MMKV (via TanStack Query persistence) |
| User-created records, offline data, anything queried | SQLite (optionally with Drizzle or WatermelonDB) |
| Tokens, passwords, keys | Secure storage (Keychain / Keystore) |
| Large files (images, audio, video) | The file system, with paths stored in SQLite |
From the field: MMKV, SQLite and WatermelonDB are all part of the offline-first toolkit I've used in production, including the offline-first workout tracking behind MOVE's 100K+ active users. Picking storage by the shape of the data, not by habit, is what keeps both startup fast and data safe.
Common storage mistakes
- One giant JSON blob under a single key. Every change rewrites everything, and one corrupt write loses it all. Split data by key, or move it to SQLite.
- Storing lists of records in key-value storage. Fine at 50 items, painful at 5,000. Collections belong in a database.
- Tokens in plain storage. Anyone with access to a rooted or jailbroken device, or an unencrypted backup, can read them. Use secure storage.
- No versioning of stored data. When the shape of stored data changes between app versions, old data crashes new code. Store a schema version and migrate on startup.
- Never clearing anything. Caches grow forever. Set limits and expiry for cached responses and files.
Migrating from AsyncStorage to MMKV
- Add
react-native-mmkv(andreact-native-nitro-modules) and create a development build. - On first launch of the new version, read each AsyncStorage key, write it to MMKV, and set a "migrated" flag.
- Switch reads and writes to MMKV.
- After a release or two, remove the migration code and the AsyncStorage dependency.
Test the migration on a device that has real data from the old version, not only on a fresh install.
FAQ
Is MMKV faster than AsyncStorage?
Yes, considerably for frequent reads and writes. MMKV uses memory-mapped files and synchronous access through JSI, while AsyncStorage is asynchronous and slower per operation. The difference matters most at startup and for state that's read often.
Should I use SQLite or MMKV in React Native?
Use MMKV for small key-value data such as settings, flags and persisted state. Use SQLite when you store collections of records that grow over time, need filtering or sorting, or must work offline. Many apps use both.
Is AsyncStorage deprecated?
AsyncStorage still works and is maintained as a community package, but it's no longer the obvious default. For new apps, MMKV is usually faster and more convenient for key-value data, and SQLite is better for structured data.
Where should I store auth tokens in React Native?
In secure storage backed by the iOS Keychain and Android Keystore, for example expo-secure-store or react-native-keychain. Don't store tokens in plain AsyncStorage or unencrypted MMKV, which aren't designed to protect secrets.
Next steps
If your app's storage has grown into a tangle of AsyncStorage keys, or you need offline data done properly, I can design the storage layer and migrate safely. Get in touch, or see how I work as a React Native developer.
Sources
- react-native-mmkv, GitHub
- AsyncStorage, React Native Community
- SQLite (expo-sqlite), Expo
- SecureStore, Expo
Related
Offline-First React Native: Lessons from an App with 100K Users
How to build offline-first React Native apps that never lose data: local-first storage, an outbox for writes, sync, conflicts and testing bad networks.
WatermelonDB vs SQLite vs Realm in 2026
WatermelonDB vs SQLite vs Realm for React Native in 2026: performance, sync, the end of Atlas Device Sync, and which local database to choose.
State Management in React Native 2026: Zustand vs Redux Toolkit vs Jotai
React Native state management in 2026: separating server and client state, then Zustand vs Redux Toolkit vs Jotai vs Legend State, with clear picks.
