diff --git a/keyserver/src/scripts/create-relationships.js b/keyserver/src/scripts/create-relationships.js
--- a/keyserver/src/scripts/create-relationships.js
+++ b/keyserver/src/scripts/create-relationships.js
@@ -50,7 +50,10 @@
     });
   }
 
-  await saveMemberships(rowsToSave);
+  await saveMemberships({
+    toSave: rowsToSave,
+    updateMembershipsLastMessage: false,
+  });
 }
 
 async function createKnowOfRelationships() {
diff --git a/keyserver/src/updaters/thread-permission-updaters.js b/keyserver/src/updaters/thread-permission-updaters.js
--- a/keyserver/src/updaters/thread-permission-updaters.js
+++ b/keyserver/src/updaters/thread-permission-updaters.js
@@ -11,10 +11,12 @@
   getRoleForPermissions,
 } from 'lib/permissions/thread-permissions.js';
 import type { CalendarQuery } from 'lib/types/entry-types.js';
+import { messageTypes } from 'lib/types/message-types-enum.js';
 import type {
   ThreadPermissionsBlob,
   ThreadRolePermissionsBlob,
 } from 'lib/types/thread-permission-types.js';
+import { threadPermissions } from 'lib/types/thread-permission-types.js';
 import {
   type ThreadType,
   assertThreadType,
@@ -881,7 +883,15 @@
 
 const membershipInsertBatchSize = 50;
 
-async function saveMemberships(toSave: $ReadOnlyArray<MembershipRowToSave>) {
+const visibleExtractString = `$.${threadPermissions.VISIBLE}.value`;
+
+async function saveMemberships({
+  toSave,
+  updateMembershipsLastMessage,
+}: {
+  toSave: $ReadOnlyArray<MembershipRowToSave>,
+  updateMembershipsLastMessage: boolean,
+}) {
   if (toSave.length === 0) {
     return;
   }
@@ -933,6 +943,79 @@
     `;
     await dbQuery(query);
   }
+
+  if (!updateMembershipsLastMessage) {
+    return;
+  }
+
+  const joinRows = toSave
+    .filter(row => row.intent === 'join')
+    .map(row => [row.userID, row.threadID, row.unread]);
+
+  if (joinRows.length === 0) {
+    return;
+  }
+
+  const joinedUserThreadPairs = joinRows.map(([user, thread]) => [
+    user,
+    thread,
+  ]);
+
+  const unreadUserThreadPairs = joinRows
+    .filter(([, , unread]) => !!unread)
+    .map(([user, thread]) => [user, thread]);
+
+  let lastReadMessageExpression;
+  if (unreadUserThreadPairs.length === 0) {
+    lastReadMessageExpression = SQL`
+      GREATEST(COALESCE(all_users_query.message, 0), 
+        COALESCE(last_subthread_message_for_user_query.message, 0))
+    `;
+  } else {
+    lastReadMessageExpression = SQL`
+      (CASE 
+        WHEN ((mm.user, mm.thread) in (${unreadUserThreadPairs})) THEN 0
+        ELSE GREATEST(COALESCE(all_users_query.message, 0), 
+          COALESCE(last_subthread_message_for_user_query.message, 0))
+      END)
+    `;
+  }
+
+  // We join two subqueries with the memberships table:
+  // - the first subquery calculates the oldest non-CREATE_SUB_THREAD
+  //   message, which is the same for all users
+  // - the second subquery calculates the oldest CREATE_SUB_THREAD messages,
+  //   which can be different for each user because of visibility permissions
+  // Then we set the `last_message` column to the greater value of the two.
+  // For `last_read_message` we do the same but only if the user should have
+  // the "unread" status set for this thread.
+  const query = SQL`
+    UPDATE memberships mm
+    LEFT JOIN (
+      SELECT messages.thread, MAX(messages.id) AS message
+      FROM messages
+      WHERE messages.type != ${messageTypes.CREATE_SUB_THREAD}
+      GROUP BY messages.thread 
+    ) all_users_query ON mm.thread = all_users_query.thread
+    LEFT JOIN (
+      SELECT m.thread, stm.user, MAX(m.id) AS message 
+      FROM messages m
+      LEFT JOIN memberships stm ON m.type = ${messageTypes.CREATE_SUB_THREAD}
+        AND stm.thread = m.content
+      WHERE JSON_EXTRACT(stm.permissions, ${visibleExtractString}) IS TRUE
+      GROUP BY m.thread, stm.user
+    ) last_subthread_message_for_user_query 
+    ON mm.thread = last_subthread_message_for_user_query.thread 
+      AND mm.user = last_subthread_message_for_user_query.user
+    SET
+      mm.last_message = GREATEST(COALESCE(all_users_query.message, 0), 
+        COALESCE(last_subthread_message_for_user_query.message, 0)),
+      mm.last_read_message =
+  `;
+  query.append(lastReadMessageExpression);
+  query.append(SQL`WHERE (mm.user, mm.thread) IN (${joinedUserThreadPairs});`);
+
+  await dbQuery(query);
 }
 
 async function deleteMemberships(
@@ -989,10 +1072,12 @@
     changedThreadIDs = new Set(),
     calendarQuery,
     updatesForCurrentSession = 'return',
+    updateMembershipsLastMessage = false,
   }: {
     +changedThreadIDs?: Set<string>,
     +calendarQuery?: ?CalendarQuery,
     +updatesForCurrentSession?: UpdatesForCurrentSession,
+    +updateMembershipsLastMessage?: boolean,
   } = emptyCommitMembershipChangesetConfig,
 ): Promise<ChangesetCommitResult> {
   if (!viewer.loggedIn) {
@@ -1053,7 +1138,7 @@
 
   const [updateDatas] = await Promise.all([
     updateChangedUndirectedRelationships(relationshipRows),
-    saveMemberships(toSave),
+    saveMemberships({ toSave, updateMembershipsLastMessage }),
     deleteMemberships(toDelete),
     rescindPushNotifsForMemberDeletion(toRescindPushNotifs),
   ]);
diff --git a/keyserver/src/updaters/thread-updaters.js b/keyserver/src/updaters/thread-updaters.js
--- a/keyserver/src/updaters/thread-updaters.js
+++ b/keyserver/src/updaters/thread-updaters.js
@@ -759,6 +759,9 @@
       Object.keys(sqlUpdate).length > 0
         ? new Set([request.threadID])
         : new Set(),
+    // last_message will be updated automatically if we send a message,
+    // so we only need to handle it here when we silence new messages
+    updateMembershipsLastMessage: silenceMessages,
   });
 
   let newMessageInfos = [];