diff --git a/lib/reducers/dm-operations-queue-reducer.js b/lib/reducers/dm-operations-queue-reducer.js
--- a/lib/reducers/dm-operations-queue-reducer.js
+++ b/lib/reducers/dm-operations-queue-reducer.js
@@ -1,6 +1,9 @@
 // @flow
 
+import _mapValues from 'lodash/fp/mapValues.js';
+
 import {
+  pruneDMOpsQueueActionType,
   type QueuedDMOperations,
   queueDMOpsActionType,
 } from '../types/dm-ops.js';
@@ -11,14 +14,26 @@
   action: BaseAction,
 ): QueuedDMOperations {
   if (action.type === queueDMOpsActionType) {
-    const { threadID, operation } = action.payload;
+    const { threadID, operation, timestamp } = action.payload;
     return {
       ...store,
       operations: {
         ...store.operations,
-        [threadID]: [...(store.operations[threadID] ?? []), operation],
+        [threadID]: [
+          ...(store.operations[threadID] ?? []),
+          { operation, timestamp },
+        ],
       },
     };
+  } else if (action.type === pruneDMOpsQueueActionType) {
+    return {
+      ...store,
+      operations: _mapValues(operations =>
+        operations.filter(
+          op => op.timestamp >= action.payload.pruneMaxTimestamp,
+        ),
+      )(store.operations),
+    };
   }
   return store;
 }
diff --git a/lib/shared/dm-ops/dm-ops-queue-pruner.react.js b/lib/shared/dm-ops/dm-ops-queue-pruner.react.js
new file mode 100644
--- /dev/null
+++ b/lib/shared/dm-ops/dm-ops-queue-pruner.react.js
@@ -0,0 +1,37 @@
+// @flow
+import * as React from 'react';
+
+import { pruneDMOpsQueueActionType } from '../../types/dm-ops.js';
+import { useDispatch } from '../../utils/redux-utils.js';
+
+const PRUNING_FREQUENCY = 60 * 60 * 1000;
+const FIRST_PRUNING_DELAY = 10 * 60 * 1000;
+const QUEUED_OPERATION_TTL = 3 * 24 * 60 * 60 * 1000;
+
+function DMOpsQueuePruner(): React.Node {
+  const dispatch = useDispatch();
+
+  const prune = React.useCallback(() => {
+    const now = Date.now();
+    dispatch({
+      type: pruneDMOpsQueueActionType,
+      payload: {
+        pruneMaxTime: now - QUEUED_OPERATION_TTL,
+      },
+    });
+  }, [dispatch]);
+
+  React.useEffect(() => {
+    const timeoutID = setTimeout(prune, FIRST_PRUNING_DELAY);
+    const intervalID = setInterval(prune, PRUNING_FREQUENCY);
+
+    return () => {
+      clearTimeout(timeoutID);
+      clearInterval(intervalID);
+    };
+  }, [prune]);
+
+  return null;
+}
+
+export { DMOpsQueuePruner };
diff --git a/lib/shared/dm-ops/process-dm-ops.js b/lib/shared/dm-ops/process-dm-ops.js
--- a/lib/shared/dm-ops/process-dm-ops.js
+++ b/lib/shared/dm-ops/process-dm-ops.js
@@ -45,6 +45,7 @@
             payload: {
               operation: dmOp,
               threadID: processingCheckResult.reason.threadID,
+              timestamp: Date.now(),
             },
           });
         }
diff --git a/lib/tunnelbroker/tunnelbroker-context.js b/lib/tunnelbroker/tunnelbroker-context.js
--- a/lib/tunnelbroker/tunnelbroker-context.js
+++ b/lib/tunnelbroker/tunnelbroker-context.js
@@ -9,6 +9,7 @@
 import { PeerToPeerMessageHandler } from './peer-to-peer-message-handler.js';
 import type { SecondaryTunnelbrokerConnection } from './secondary-tunnelbroker-connection.js';
 import { tunnnelbrokerURL } from '../facts/tunnelbroker.js';
+import { DMOpsQueuePruner } from '../shared/dm-ops/dm-ops-queue-pruner.react.js';
 import { IdentityClientContext } from '../shared/identity-client-context.js';
 import { tunnelbrokerHeartbeatTimeout } from '../shared/timeouts.js';
 import { isWebPlatform } from '../types/device-types.js';
@@ -461,6 +462,7 @@
         socketSend={socketSend}
       />
       <PeerToPeerProvider>{children}</PeerToPeerProvider>
+      <DMOpsQueuePruner />
     </TunnelbrokerContext.Provider>
   );
 }
diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js
--- a/lib/types/dm-ops.js
+++ b/lib/types/dm-ops.js
@@ -313,6 +313,12 @@
 export type QueueDMOpsPayload = {
   +operation: DMOperation,
   +threadID: string,
+  +timestamp: number,
+};
+
+export const pruneDMOpsQueueActionType = 'PRUNE_DM_OPS_QUEUE';
+export type PruneDMOpsQueuePayload = {
+  +pruneMaxTimestamp: number,
 };
 
 export const scheduleP2PMessagesActionType = 'SCHEDULE_P2P_MESSAGES';
@@ -323,6 +329,9 @@
 
 export type QueuedDMOperations = {
   +operations: {
-    +[threadID: string]: $ReadOnlyArray<DMOperation>,
+    +[threadID: string]: $ReadOnlyArray<{
+      +operation: DMOperation,
+      +timestamp: number,
+    }>,
   },
 };
diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js
--- a/lib/types/redux-types.js
+++ b/lib/types/redux-types.js
@@ -45,6 +45,7 @@
   ProcessDMOpsPayload,
   QueuedDMOperations,
   QueueDMOpsPayload,
+  PruneDMOpsQueuePayload,
 } from './dm-ops.js';
 import type { DraftStore } from './draft-types.js';
 import type { EnabledApps, SupportedApps } from './enabled-apps.js';
@@ -1581,7 +1582,8 @@
           +deviceToken: string,
         },
       }
-    | { +type: 'QUEUE_DM_OPS', +payload: QueueDMOpsPayload },
+    | { +type: 'QUEUE_DM_OPS', +payload: QueueDMOpsPayload }
+    | { +type: 'PRUNE_DM_OPS_QUEUE', +payload: PruneDMOpsQueuePayload },
 }>;
 
 export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string);