diff --git a/keyserver/src/responders/message-responders.js b/keyserver/src/responders/message-responders.js
--- a/keyserver/src/responders/message-responders.js
+++ b/keyserver/src/responders/message-responders.js
@@ -24,7 +24,7 @@
   type FetchPinnedMessagesRequest,
   type FetchPinnedMessagesResult,
   type SearchMessagesResponse,
-  type SearchMessagesRequest,
+  type SearchMessagesKeyserverRequest,
 } from 'lib/types/message-types.js';
 import type { EditMessageData } from 'lib/types/messages/edit.js';
 import type { ReactionMessageData } from 'lib/types/messages/reaction.js';
@@ -396,8 +396,8 @@
   return await fetchPinnedMessageInfos(viewer, request);
 }
 
-export const searchMessagesResponderInputValidator: TInterface<SearchMessagesRequest> =
-  tShape<SearchMessagesRequest>({
+export const searchMessagesResponderInputValidator: TInterface<SearchMessagesKeyserverRequest> =
+  tShape<SearchMessagesKeyserverRequest>({
     query: t.String,
     threadID: tID,
     cursor: t.maybe(tID),
@@ -405,7 +405,7 @@
 
 async function searchMessagesResponder(
   viewer: Viewer,
-  request: SearchMessagesRequest,
+  request: SearchMessagesKeyserverRequest,
 ): Promise<SearchMessagesResponse> {
   return await searchMessagesInSingleChat(
     request.query,
diff --git a/lib/actions/message-actions.js b/lib/actions/message-actions.js
--- a/lib/actions/message-actions.js
+++ b/lib/actions/message-actions.js
@@ -1,9 +1,11 @@
 // @flow
 
 import invariant from 'invariant';
+import * as React from 'react';
 
 import type { CallSingleKeyserverEndpointResultInfo } from '../keyserver-conn/call-single-keyserver-endpoint.js';
 import {
+  extractKeyserverIDFromIDOptional,
   extractKeyserverIDFromID,
   sortThreadIDsPerKeyserver,
 } from '../keyserver-conn/keyserver-call-utils.js';
@@ -19,16 +21,20 @@
   FetchPinnedMessagesRequest,
   FetchPinnedMessagesResult,
   SearchMessagesRequest,
+  SearchMessagesKeyserverRequest,
   SearchMessagesResponse,
   FetchMessageInfosRequest,
   RawMessageInfo,
   MessageTruncationStatuses,
 } from '../types/message-types.js';
+import { defaultNumberPerThread } from '../types/message-types.js';
 import type { MediaMessageServerDBContent } from '../types/messages/media.js';
 import type {
   ToggleMessagePinRequest,
   ToggleMessagePinResult,
 } from '../types/thread-types.js';
+import { getConfig } from '../utils/config.js';
+import { translateClientDBMessageInfoToRawMessageInfo } from '../utils/message-ops-utils.js';
 
 const fetchMessagesBeforeCursorActionTypes = Object.freeze({
   started: 'FETCH_MESSAGES_BEFORE_CURSOR_STARTED',
@@ -452,7 +458,9 @@
 const searchMessages =
   (
     callKeyserverEndpoint: CallKeyserverEndpoint,
-  ): ((input: SearchMessagesRequest) => Promise<SearchMessagesResponse>) =>
+  ): ((
+    input: SearchMessagesKeyserverRequest,
+  ) => Promise<SearchMessagesResponse>) =>
   async input => {
     const keyserverID = extractKeyserverIDFromID(input.threadID);
     const requests = { [keyserverID]: input };
@@ -468,7 +476,38 @@
 function useSearchMessages(): (
   input: SearchMessagesRequest,
 ) => Promise<SearchMessagesResponse> {
-  return useKeyserverCall(searchMessages);
+  const thinThreadCallback = useKeyserverCall(searchMessages);
+  return React.useCallback(
+    async (input: SearchMessagesRequest) => {
+      const isThreadThin = !!extractKeyserverIDFromIDOptional(input.threadID);
+
+      if (isThreadThin) {
+        return await thinThreadCallback({
+          query: input.query,
+          threadID: input.threadID,
+          cursor: input.messageIDcursor,
+        });
+      }
+
+      const { sqliteAPI } = getConfig();
+      const timestampCursor = input.timestampCursor?.toString();
+      const clientDBMessageInfos = await sqliteAPI.searchMessages(
+        input.query,
+        input.threadID,
+        timestampCursor,
+        input.messageIDcursor,
+      );
+
+      const messages = clientDBMessageInfos.map(
+        translateClientDBMessageInfoToRawMessageInfo,
+      );
+      return {
+        endReached: messages.length < defaultNumberPerThread,
+        messages,
+      };
+    },
+    [thinThreadCallback],
+  );
 }
 
 const toggleMessagePinActionTypes = Object.freeze({
diff --git a/lib/shared/search-utils.js b/lib/shared/search-utils.js
--- a/lib/shared/search-utils.js
+++ b/lib/shared/search-utils.js
@@ -329,13 +329,21 @@
     threadID: string,
   ) => mixed,
   queryID: number,
-  cursor?: ?string,
+  timestampCursor?: ?number,
+  messageIDCursor?: ?string,
 ) => void {
   const callSearchMessages = useSearchMessagesAction();
   const dispatchActionPromise = useDispatchActionPromise();
 
   return React.useCallback(
-    (query, threadID, onResultsReceived, queryID, cursor) => {
+    (
+      query,
+      threadID,
+      onResultsReceived,
+      queryID,
+      timestampCursor,
+      messageIDcursor,
+    ) => {
       const searchMessagesPromise = (async () => {
         if (query === '') {
           onResultsReceived([], true, queryID, threadID);
@@ -344,7 +352,8 @@
         const { messages, endReached } = await callSearchMessages({
           query,
           threadID,
-          cursor,
+          timestampCursor,
+          messageIDcursor,
         });
         onResultsReceived(messages, endReached, queryID, threadID);
       })();
diff --git a/lib/types/message-types.js b/lib/types/message-types.js
--- a/lib/types/message-types.js
+++ b/lib/types/message-types.js
@@ -692,6 +692,13 @@
 };
 
 export type SearchMessagesRequest = {
+  +query: string,
+  +threadID: string,
+  +timestampCursor?: ?number,
+  +messageIDcursor?: ?string,
+};
+
+export type SearchMessagesKeyserverRequest = {
   +query: string,
   +threadID: string,
   +cursor?: ?string,
diff --git a/native/search/message-search.react.js b/native/search/message-search.react.js
--- a/native/search/message-search.react.js
+++ b/native/search/message-search.react.js
@@ -47,6 +47,7 @@
   }, [props.navigation, clearQuery]);
 
   const [lastID, setLastID] = React.useState<?string>();
+  const [lastTimestamp, setLastTimestamp] = React.useState<?number>();
   const [searchResults, setSearchResults] = React.useState<
     $ReadOnlyArray<RawMessageInfo>,
   >([]);
@@ -74,6 +75,7 @@
   React.useEffect(() => {
     setSearchResults([]);
     setLastID(undefined);
+    setLastTimestamp(undefined);
     setEndReached(false);
   }, [query, searchMessages]);
 
@@ -84,9 +86,17 @@
       threadInfo.id,
       appendSearchResults,
       queryIDRef.current,
+      lastTimestamp,
       lastID,
     );
-  }, [appendSearchResults, lastID, query, searchMessages, threadInfo.id]);
+  }, [
+    appendSearchResults,
+    lastID,
+    query,
+    searchMessages,
+    threadInfo.id,
+    lastTimestamp,
+  ]);
 
   const userInfos = useSelector(state => state.userStore.userInfos);
 
@@ -190,8 +200,10 @@
     if (endReached) {
       return;
     }
-    setLastID(oldestMessageID(measuredMessages));
-  }, [endReached, measuredMessages, setLastID]);
+    const oldest = oldestMessage(measuredMessages);
+    setLastID(oldest?.id);
+    setLastTimestamp(oldest?.time);
+  }, [endReached, measuredMessages]);
 
   const styles = useStyles(unboundStyles);
 
@@ -213,10 +225,10 @@
   );
 }
 
-function oldestMessageID(data: $ReadOnlyArray<ChatMessageItemWithHeight>) {
+function oldestMessage(data: $ReadOnlyArray<ChatMessageItemWithHeight>) {
   for (let i = data.length - 1; i >= 0; i--) {
     if (data[i].itemType === 'message' && data[i].messageInfo.id) {
-      return data[i].messageInfo.id;
+      return data[i].messageInfo;
     }
   }
   return undefined;
diff --git a/web/search/message-search-state-provider.react.js b/web/search/message-search-state-provider.react.js
--- a/web/search/message-search-state-provider.react.js
+++ b/web/search/message-search-state-provider.react.js
@@ -42,6 +42,9 @@
   const lastIDs = React.useRef<{
     [threadID: string]: string,
   }>({});
+  const lastTimestamps = React.useRef<{
+    [threadID: string]: number,
+  }>({});
 
   const setEndReached = React.useCallback((threadID: string) => {
     endsReached.current.add(threadID);
@@ -59,9 +62,10 @@
 
   const appendResult = React.useCallback(
     (result: $ReadOnlyArray<RawMessageInfo>, threadID: string) => {
-      const lastMessageID = oldestMessageID(result);
-      if (lastMessageID) {
-        lastIDs.current[threadID] = lastMessageID;
+      const lastMessage = oldestMessage(result);
+      if (lastMessage?.id) {
+        lastIDs.current[threadID] = lastMessage.id;
+        lastTimestamps.current[threadID] = lastMessage.time;
       }
       setResults(prevResults => {
         const prevThreadResults = prevResults[threadID] ?? [];
@@ -76,6 +80,7 @@
     (threadID: string) => {
       loading.current = false;
       delete lastIDs.current[threadID];
+      delete lastTimestamps.current[threadID];
       removeEndReached(threadID);
       setResults(prevResults => {
         const { [threadID]: deleted, ...newState } = prevResults;
@@ -149,6 +154,7 @@
         threadID,
         appendResults,
         queryIDRef.current,
+        lastTimestamps.current[threadID],
         lastIDs.current[threadID],
       );
     },
@@ -185,13 +191,13 @@
   );
 }
 
-function oldestMessageID(data: $ReadOnlyArray<RawMessageInfo>) {
+function oldestMessage(data: $ReadOnlyArray<RawMessageInfo>) {
   if (!data) {
     return undefined;
   }
   for (let i = data.length - 1; i >= 0; i--) {
     if (data[i].type === messageTypes.TEXT) {
-      return data[i].id;
+      return data[i];
     }
   }
   return undefined;