12 Nov 2026 · 5 min read

AsyncStorage vs MMKV vs expo-sqlite/kv-store

AsyncStorage, MMKV or expo-sqlite/kv-store in Expo: Expo Go support, sync vs async API, speed, encryption, and how to migrate between them.

An Expo app keeps small key/value data in one of three places: AsyncStorage, MMKV, or expo-sqlite/kv-store. For most apps, pick expo-sqlite/kv-store. It runs in Expo Go, has the same API as AsyncStorage, and adds synchronous calls on native. Pick MMKV when you already need a development build and reads are on a hot path. Keep AsyncStorage for a library that requires it. Tokens and passwords belong in expo-secure-store, whichever store you choose for the rest.

One storage adapter, three backendsBuilt-in stores, zustand persist, Jotai and i18n call one storage adapter, which sits on expo-sqlite/kv-store, MMKV or AsyncStorage.built-in storeszustand persistJotaii18n@/lib/storage adapterget · set · deleteexpo-sqlite/kv-storedefault · Expo Gosync + asyncMMKVsync · dev buildAsyncStorageasync · Expo Go

The comparison in one table

AsyncStorageMMKVexpo-sqlite/kv-store
Runs in Expo GoYesNoYes
Sync APINo, async onlyYes, every callYes on native; async only on web in ReadyNative
Relative speedThe async baselineFastest; its README claims about 30x AsyncStorageSQLite-backed; Expo publishes no numbers
EncryptionNone; Expo calls it unencryptedOptional, with an encryptionKeyNot described in Expo's docs
Native build neededNoYes, a development buildNo

expo-sqlite/kv-store: AsyncStorage's API on SQLite

expo-sqlite/kv-store is Expo's drop-in replacement for AsyncStorage. Expo documents its Storage object as having the same API as AsyncStorage, backed by SQLite, and expo-sqlite is included in Expo Go. Switching an app over is an import change:

- import AsyncStorage from "@react-native-async-storage/async-storage";
+ import AsyncStorage from "expo-sqlite/kv-store";

The extra is the synchronous calls. On native, setItemSync, getItemSync and removeItemSync return at once, which is why ReadyNative's built-in stores can hydrate before first paint:

import Storage from "expo-sqlite/kv-store";

Storage.setItemSync("theme", "dark");
const theme = Storage.getItemSync("theme"); // "dark", or null when unset

Web is the exception. Expo's SQLite on web needs Metro set up for wasm files and the COOP and COEP headers for SharedArrayBuffer, so ReadyNative's adapter exposes only the async methods there.

Switching does not move existing data. AsyncStorage keeps its own store, so values already written stay there until you copy them. The FAQ below has the loop.

MMKV: the fastest store, with a development build

react-native-mmkv 4 is a synchronous, memory-mapped key/value store built on Nitro modules. Its calls return at once, with no Promise, and its README claims about 30 times the speed of AsyncStorage on the author's benchmark. It is native code, so Expo Go cannot load it. Make a development build with npx expo run:ios or npx expo run:android, or with bunx eas-cli build --profile development, and read Expo Go vs development build for why.

import { createMMKV } from "react-native-mmkv";

export const storage = createMMKV({ id: "readynative" });

storage.set("theme", "dark");
storage.getString("theme"); // "dark"

Encryption is opt-in. By default MMKV writes plain text and relies on the iOS and Android app sandbox. Pass an encryptionKey to createMMKV and the data is encrypted with AES-128, or with AES-256 when you also set encryptionType to AES-256. Credentials still belong in expo-secure-store, which uses the Keychain and the Android Keystore.

MMKV has its own AsyncStorage migration guide: a one-time loop that copies each key and removes it from AsyncStorage afterwards.

expo-secure-store: secrets, not a general store

expo-secure-store encrypts small key/value pairs on the device. On Android the values go in SharedPreferences, encrypted with the Android Keystore system; on iOS they go in the Keychain. It is included in Expo Go, and its API is async:

import * as SecureStore from "expo-secure-store";

await SecureStore.setItemAsync("refresh_token", token);
const saved = await SecureStore.getItemAsync("refresh_token");
await SecureStore.deleteItemAsync("refresh_token");

Keys may contain letters, digits, dots, dashes and underscores. Expo enforces no size limit, but some older iOS releases refused values above roughly 2048 bytes, so store a token there, not a document. On iOS the Keychain also keeps data across a reinstall with the same bundle id, which Expo says not to rely on.

ReadyNative uses it where a session lives, not in the storage module. The Clerk module keeps its client JWT in SecureStore through Clerk's token cache, and the Better Auth module keeps its session cookie there. The auth comparison lists where each option keeps its session. None of the three storage options touch it.

When each one wins

How ReadyNative wires it

The storage category has three options, and each one writes the same file, src/lib/storage.ts. Your code imports storage from @/lib/storage and talks to the StorageAdapter interface in src/lib/storage-adapter.ts. The built-in stores, zustand persist, Jotai and i18n all go through that adapter, so they do not change when you switch. The default is kv-store.

bun run setup --storage mmkv

That replaces src/lib/storage.ts and nothing else. Keys stay the same readynative:* strings, but the data lives in the new backend, so existing values do not move and users start fresh. The template has no migration, so copy any data you need first. The option pages are on the kv-store stack page.

Read next: Expo Go vs development build, for why MMKV needs a build, then the storage module docs.

FAQ

Is expo-sqlite/kv-store a drop-in replacement for AsyncStorage?

Expo documents it that way: Storage has the same API as AsyncStorage, so for most code you change only the import. It also adds synchronous calls, which ReadyNative exposes on native and not on web.

Does MMKV work in Expo Go?

No. react-native-mmkv is a native Nitro module, and Expo Go does not load it. Make a development build with npx expo run:ios, npx expo run:android or bunx eas-cli build --profile development, and run the app from that build.

Is AsyncStorage encrypted?

No. Expo describes AsyncStorage as unencrypted. MMKV encrypts only when you pass an encryptionKey, and expo-secure-store uses the Keychain and the Android Keystore, so put tokens and passwords there.

How do I move existing AsyncStorage data to expo-sqlite/kv-store?

Changing the import does not copy old values; they stay in AsyncStorage. Run a one-time loop while both imports exist: for each key from AsyncStorage.getAllKeys(), read AsyncStorage.getItem(key) and write it with Storage.setItemAsync(key, value).

How large can a SecureStore value be?

Expo does not enforce a limit, but some older iOS releases refused values above roughly 2048 bytes. Keep tokens small, and handle the rejection that setItemAsync returns when a value cannot be stored.

More guides