diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js
--- a/lib/actions/user-actions.js
+++ b/lib/actions/user-actions.js
@@ -3,6 +3,7 @@
 import invariant from 'invariant';
 import * as React from 'react';
 
+import { useUserIdentityCache } from '../components/user-identity-cache.react.js';
 import {
   useBroadcastDeviceListUpdates,
   useBroadcastAccountDeletion,
@@ -1312,10 +1313,8 @@
 function useFindUserIdentities(): (
   userIDs: $ReadOnlyArray<string>,
 ) => Promise<UserIdentitiesResponse> {
-  const client = React.useContext(IdentityClientContext);
-  const identityClient = client?.identityClient;
-  invariant(identityClient, 'Identity client should be set');
-  return identityClient.findUserIdentities;
+  const userIdentityCache = useUserIdentityCache();
+  return userIdentityCache.getUserIdentities;
 }
 
 const versionSupportedByIdentityActionTypes = Object.freeze({
diff --git a/lib/components/user-identity-cache.react.js b/lib/components/user-identity-cache.react.js
new file mode 100644
--- /dev/null
+++ b/lib/components/user-identity-cache.react.js
@@ -0,0 +1,247 @@
+// @flow
+
+import invariant from 'invariant';
+import * as React from 'react';
+
+import { IdentityClientContext } from '../shared/identity-client-context.js';
+import type {
+  UserIdentitiesResponse,
+  Identity,
+} from '../types/identity-service-types.js';
+import sleep from '../utils/sleep.js';
+
+const cacheTimeout = 24 * 60 * 60 * 1000; // one day
+const failedQueryCacheTimeout = 5 * 60 * 1000; // five minutes
+const queryTimeout = 10 * 1000; // ten seconds
+
+async function throwOnTimeout(identifier: string) {
+  await sleep(queryTimeout);
+  throw new Error(`User identity fetch for ${identifier} timed out`);
+}
+
+function getUserIdentitiesResponseFromResults(
+  userIDs: $ReadOnlyArray<string>,
+  results: $ReadOnlyArray<?UserIdentityResult>,
+): UserIdentitiesResponse {
+  const response: {
+    identities: { [userID: string]: Identity },
+    reservedUserIdentifiers: { [userID: string]: string },
+  } = {
+    identities: {},
+    reservedUserIdentifiers: {},
+  };
+  for (let i = 0; i < userIDs.length; i++) {
+    const userID = userIDs[i];
+    const result = results[i];
+    if (!result) {
+      continue;
+    } else if (result.type === 'registered') {
+      response.identities[userID] = result.identity;
+    } else if (result.type === 'reserved') {
+      response.reservedUserIdentifiers[userID] = result.identifier;
+    }
+  }
+  return response;
+}
+
+type UserIdentityCache = {
+  +getUserIdentities: (
+    userIDs: $ReadOnlyArray<string>,
+  ) => Promise<UserIdentitiesResponse>,
+  +getCachedUserIdentity: (userID: string) => ?UserIdentityResult,
+};
+
+type UserIdentityResult =
+  | { +type: 'registered', +identity: Identity }
+  | { +type: 'reserved', +identifier: string };
+type UserIdentityCacheEntry = {
+  +userID: string,
+  +expirationTime: number,
+  +result: ?UserIdentityResult | Promise<?UserIdentityResult>,
+};
+
+const UserIdentityCacheContext: React.Context<?UserIdentityCache> =
+  React.createContext<?UserIdentityCache>();
+
+type Props = {
+  +children: React.Node,
+};
+function UserIdentityCacheProvider(props: Props): React.Node {
+  const userIdentityCacheRef = React.useRef<
+    Map<string, UserIdentityCacheEntry>,
+  >(new Map());
+
+  const getCachedUserIdentityEntry = React.useCallback(
+    (userID: string): ?UserIdentityCacheEntry => {
+      const cache = userIdentityCacheRef.current;
+
+      const cacheResult = cache.get(userID);
+      if (!cacheResult) {
+        return undefined;
+      }
+
+      const { expirationTime } = cacheResult;
+      if (expirationTime <= Date.now()) {
+        cache.delete(userID);
+        return undefined;
+      }
+
+      return cacheResult;
+    },
+    [],
+  );
+
+  const getCachedUserIdentity = React.useCallback(
+    (userID: string): ?UserIdentityResult => {
+      const cacheResult = getCachedUserIdentityEntry(userID);
+      if (!cacheResult) {
+        return undefined;
+      }
+
+      const { result } = cacheResult;
+      if (typeof result !== 'object' || result instanceof Promise || !result) {
+        return undefined;
+      }
+
+      return result;
+    },
+    [getCachedUserIdentityEntry],
+  );
+
+  const client = React.useContext(IdentityClientContext);
+  const identityClient = client?.identityClient;
+  invariant(identityClient, 'Identity client should be set');
+  const { findUserIdentities } = identityClient;
+
+  const getUserIdentities = React.useCallback(
+    async (
+      userIDs: $ReadOnlyArray<string>,
+    ): Promise<UserIdentitiesResponse> => {
+      const cacheMatches = userIDs.map(getCachedUserIdentityEntry);
+      const cacheResultsPromise = Promise.all(
+        cacheMatches.map(match =>
+          Promise.resolve(match ? match.result : match),
+        ),
+      );
+      if (cacheMatches.every(Boolean)) {
+        const results = await cacheResultsPromise;
+        return getUserIdentitiesResponseFromResults(userIDs, results);
+      }
+
+      const needFetch = [];
+      for (let i = 0; i < userIDs.length; i++) {
+        const userID = userIDs[i];
+        const cacheMatch = cacheMatches[i];
+        if (!cacheMatch) {
+          needFetch.push(userID);
+        }
+      }
+
+      const fetchUserIdentitiesPromise = (async () => {
+        let userIdentities: ?UserIdentitiesResponse;
+        try {
+          userIdentities = await Promise.race([
+            findUserIdentities(needFetch),
+            throwOnTimeout(`user identities for ${JSON.stringify(needFetch)}`),
+          ]);
+        } catch (e) {
+          console.log(e);
+        }
+
+        const resultMap = new Map<string, ?UserIdentityResult>();
+        for (let i = 0; i < needFetch.length; i++) {
+          const userID = needFetch[i];
+          if (!userIdentities) {
+            resultMap.set(userID, null);
+            continue;
+          }
+          const identityMatch = userIdentities.identities[userID];
+          if (identityMatch) {
+            resultMap.set(userID, {
+              type: 'registered',
+              identity: identityMatch,
+            });
+            continue;
+          }
+          const reservedIdentifierMatch =
+            userIdentities.reservedUserIdentifiers[userID];
+          if (reservedIdentifierMatch) {
+            resultMap.set(userID, {
+              type: 'reserved',
+              identifier: reservedIdentifierMatch,
+            });
+            continue;
+          }
+          resultMap.set(userID, null);
+        }
+        return resultMap;
+      })();
+
+      const cache = userIdentityCacheRef.current;
+      for (let i = 0; i < needFetch.length; i++) {
+        const userID = needFetch[i];
+        const fetchUserIdentityPromise = (async () => {
+          const resultMap = await fetchUserIdentitiesPromise;
+          return resultMap.get(userID) ?? null;
+        })();
+        cache.set(userID, {
+          userID,
+          expirationTime: Date.now() + queryTimeout * 2,
+          result: fetchUserIdentityPromise,
+        });
+      }
+
+      return (async () => {
+        const [resultMap, cacheResults] = await Promise.all([
+          fetchUserIdentitiesPromise,
+          cacheResultsPromise,
+        ]);
+        for (let i = 0; i < needFetch.length; i++) {
+          const userID = needFetch[i];
+          const userIdentity = resultMap.get(userID);
+          const timeout =
+            userIdentity === null ? failedQueryCacheTimeout : cacheTimeout;
+          cache.set(userID, {
+            userID,
+            expirationTime: Date.now() + timeout,
+            result: userIdentity,
+          });
+        }
+
+        const results = [];
+        for (let i = 0; i < userIDs.length; i++) {
+          const cachedResult = cacheResults[i];
+          if (cachedResult) {
+            results.push(cachedResult);
+          } else {
+            results.push(resultMap.get(userIDs[i]));
+          }
+        }
+        return getUserIdentitiesResponseFromResults(userIDs, results);
+      })();
+    },
+    [getCachedUserIdentityEntry, findUserIdentities],
+  );
+
+  const value = React.useMemo(
+    () => ({
+      getUserIdentities,
+      getCachedUserIdentity,
+    }),
+    [getUserIdentities, getCachedUserIdentity],
+  );
+
+  return (
+    <UserIdentityCacheContext.Provider value={value}>
+      {props.children}
+    </UserIdentityCacheContext.Provider>
+  );
+}
+
+function useUserIdentityCache(): UserIdentityCache {
+  const context = React.useContext(UserIdentityCacheContext);
+  invariant(context, 'UserIdentityCacheContext not found');
+  return context;
+}
+
+export { UserIdentityCacheProvider, useUserIdentityCache };
diff --git a/native/root.react.js b/native/root.react.js
--- a/native/root.react.js
+++ b/native/root.react.js
@@ -34,6 +34,7 @@
 import PrekeysHandler from 'lib/components/prekeys-handler.react.js';
 import { QRAuthProvider } from 'lib/components/qr-auth-provider.react.js';
 import { StaffContextProvider } from 'lib/components/staff-provider.react.js';
+import { UserIdentityCacheProvider } from 'lib/components/user-identity-cache.react.js';
 import { DBOpsHandler } from 'lib/handlers/db-ops-handler.react.js';
 import { TunnelbrokerDeviceTokenHandler } from 'lib/handlers/tunnelbroker-device-token-handler.react.js';
 import { UserInfosHandler } from 'lib/handlers/user-infos-handler.react.js';
@@ -324,78 +325,82 @@
     <GestureHandlerRootView style={styles.app}>
       <StaffContextProvider>
         <IdentityServiceContextProvider>
-          <TunnelbrokerProvider>
-            <IdentitySearchProvider>
-              <QRAuthProvider
-                parseTunnelbrokerQRAuthMessage={parseTunnelbrokerQRAuthMessage}
-                composeTunnelbrokerQRAuthMessage={
-                  composeTunnelbrokerQRAuthMessage
-                }
-                generateAESKey={generateQRAuthAESKey}
-                performBackupRestore={performBackupRestore}
-                onLogInError={handleSecondaryDeviceLogInError}
-              >
-                <FeatureFlagsProvider>
-                  <NavContext.Provider value={navContext}>
-                    <RootContext.Provider value={rootContext}>
-                      <InputStateContainer>
-                        <MessageEditingContextProvider>
-                          <SafeAreaProvider
-                            initialMetrics={initialWindowMetrics}
-                          >
-                            <ActionSheetProvider>
-                              <ENSCacheProvider provider={provider}>
-                                <NeynarClientProvider apiKey={neynarKey}>
-                                  <MediaCacheProvider
-                                    persistence={filesystemMediaCache}
-                                  >
-                                    <EditUserAvatarProvider>
-                                      <NativeEditThreadAvatarProvider>
-                                        <MarkdownContextProvider>
-                                          <MessageSearchProvider>
-                                            <BottomSheetProvider>
-                                              <RegistrationContextProvider>
-                                                <SQLiteDataHandler />
-                                                <ConnectedStatusBar />
-                                                <ReduxPersistGate
-                                                  persistor={getPersistor()}
-                                                >
-                                                  {gated}
-                                                </ReduxPersistGate>
-                                                <PersistedStateGate>
-                                                  <KeyserverConnectionsHandler
-                                                    socketComponent={Socket}
-                                                    detectUnsupervisedBackgroundRef={
-                                                      detectUnsupervisedBackgroundRef
-                                                    }
-                                                  />
-                                                  <VersionSupportedChecker />
-                                                  <PlatformDetailsSynchronizer />
-                                                  <BackgroundIdentityLoginHandler />
-                                                  <PrekeysHandler />
-                                                  <ReportHandler />
-                                                  <AutoJoinCommunityHandler />
-                                                </PersistedStateGate>
-                                                {navigation}
-                                              </RegistrationContextProvider>
-                                            </BottomSheetProvider>
-                                          </MessageSearchProvider>
-                                        </MarkdownContextProvider>
-                                      </NativeEditThreadAvatarProvider>
-                                    </EditUserAvatarProvider>
-                                  </MediaCacheProvider>
-                                </NeynarClientProvider>
-                              </ENSCacheProvider>
-                            </ActionSheetProvider>
-                          </SafeAreaProvider>
-                        </MessageEditingContextProvider>
-                      </InputStateContainer>
-                    </RootContext.Provider>
-                  </NavContext.Provider>
-                </FeatureFlagsProvider>
-              </QRAuthProvider>
-            </IdentitySearchProvider>
-          </TunnelbrokerProvider>
+          <UserIdentityCacheProvider>
+            <TunnelbrokerProvider>
+              <IdentitySearchProvider>
+                <QRAuthProvider
+                  parseTunnelbrokerQRAuthMessage={
+                    parseTunnelbrokerQRAuthMessage
+                  }
+                  composeTunnelbrokerQRAuthMessage={
+                    composeTunnelbrokerQRAuthMessage
+                  }
+                  generateAESKey={generateQRAuthAESKey}
+                  performBackupRestore={performBackupRestore}
+                  onLogInError={handleSecondaryDeviceLogInError}
+                >
+                  <FeatureFlagsProvider>
+                    <NavContext.Provider value={navContext}>
+                      <RootContext.Provider value={rootContext}>
+                        <InputStateContainer>
+                          <MessageEditingContextProvider>
+                            <SafeAreaProvider
+                              initialMetrics={initialWindowMetrics}
+                            >
+                              <ActionSheetProvider>
+                                <ENSCacheProvider provider={provider}>
+                                  <NeynarClientProvider apiKey={neynarKey}>
+                                    <MediaCacheProvider
+                                      persistence={filesystemMediaCache}
+                                    >
+                                      <EditUserAvatarProvider>
+                                        <NativeEditThreadAvatarProvider>
+                                          <MarkdownContextProvider>
+                                            <MessageSearchProvider>
+                                              <BottomSheetProvider>
+                                                <RegistrationContextProvider>
+                                                  <SQLiteDataHandler />
+                                                  <ConnectedStatusBar />
+                                                  <ReduxPersistGate
+                                                    persistor={getPersistor()}
+                                                  >
+                                                    {gated}
+                                                  </ReduxPersistGate>
+                                                  <PersistedStateGate>
+                                                    <KeyserverConnectionsHandler
+                                                      socketComponent={Socket}
+                                                      detectUnsupervisedBackgroundRef={
+                                                        detectUnsupervisedBackgroundRef
+                                                      }
+                                                    />
+                                                    <VersionSupportedChecker />
+                                                    <PlatformDetailsSynchronizer />
+                                                    <BackgroundIdentityLoginHandler />
+                                                    <PrekeysHandler />
+                                                    <ReportHandler />
+                                                    <AutoJoinCommunityHandler />
+                                                  </PersistedStateGate>
+                                                  {navigation}
+                                                </RegistrationContextProvider>
+                                              </BottomSheetProvider>
+                                            </MessageSearchProvider>
+                                          </MarkdownContextProvider>
+                                        </NativeEditThreadAvatarProvider>
+                                      </EditUserAvatarProvider>
+                                    </MediaCacheProvider>
+                                  </NeynarClientProvider>
+                                </ENSCacheProvider>
+                              </ActionSheetProvider>
+                            </SafeAreaProvider>
+                          </MessageEditingContextProvider>
+                        </InputStateContainer>
+                      </RootContext.Provider>
+                    </NavContext.Provider>
+                  </FeatureFlagsProvider>
+                </QRAuthProvider>
+              </IdentitySearchProvider>
+            </TunnelbrokerProvider>
+          </UserIdentityCacheProvider>
         </IdentityServiceContextProvider>
       </StaffContextProvider>
     </GestureHandlerRootView>
diff --git a/web/root.js b/web/root.js
--- a/web/root.js
+++ b/web/root.js
@@ -14,6 +14,7 @@
 import IntegrityHandler from 'lib/components/integrity-handler.react.js';
 import PrekeysHandler from 'lib/components/prekeys-handler.react.js';
 import ReportHandler from 'lib/components/report-handler.react.js';
+import { UserIdentityCacheProvider } from 'lib/components/user-identity-cache.react.js';
 import { CallKeyserverEndpointProvider } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js';
 import KeyserverConnectionsHandler from 'lib/keyserver-conn/keyserver-connections-handler.js';
 import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js';
@@ -68,14 +69,16 @@
           <CallKeyserverEndpointProvider>
             <InitialReduxStateGate persistor={persistor}>
               <IdentityServiceContextProvider>
-                <Router history={history.getHistoryObject()}>
-                  <Route path="*" component={App} />
-                </Router>
-                <KeyserverConnectionsHandler socketComponent={Socket} />
-                <PrekeysHandler />
-                <SQLiteDataHandler />
-                <IntegrityHandler />
-                <ReportHandler canSendReports={true} />
+                <UserIdentityCacheProvider>
+                  <Router history={history.getHistoryObject()}>
+                    <Route path="*" component={App} />
+                  </Router>
+                  <KeyserverConnectionsHandler socketComponent={Socket} />
+                  <PrekeysHandler />
+                  <SQLiteDataHandler />
+                  <IntegrityHandler />
+                  <ReportHandler canSendReports={true} />
+                </UserIdentityCacheProvider>
               </IdentityServiceContextProvider>
             </InitialReduxStateGate>
           </CallKeyserverEndpointProvider>