diff --git a/gajim/application.py b/gajim/application.py
index f7ed67c73ef1d2b9fd66d3b4926d1b4a05399a46..4c0f516ce0c28c6cf367c4e06eb275c69bbd06b7 100644
--- a/gajim/application.py
+++ b/gajim/application.py
@@ -55,6 +55,7 @@
 from gajim.common import logger
 from gajim.common.i18n import _
 from gajim.common.contacts import LegacyContactsAPI
+from gajim.common.task_manager import TaskManager
 
 
 class GajimApplication(Gtk.Application):
@@ -218,6 +219,7 @@ def _startup(self, _application):
 
         from gajim.common.cert_store import CertificateStore
         app.cert_store = CertificateStore()
+        app.task_manager = TaskManager()
 
         # Set Application Menu
         app.app = self
diff --git a/gajim/common/app.py b/gajim/common/app.py
index 9175467eaf8769ff6786c9a3ff3eff72780a6557..62cc57d1c68440e0f70779ad9a60913ac56d6a1d 100644
--- a/gajim/common/app.py
+++ b/gajim/common/app.py
@@ -131,6 +131,8 @@
 
 cert_store = None
 
+task_manager = None
+
 # zeroconf account name
 ZEROCONF_ACC_NAME = 'Local'
 
diff --git a/gajim/common/task_manager.py b/gajim/common/task_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..11d2dec0baeace3a3199cdd986af0d3f8ac9cb90
--- /dev/null
+++ b/gajim/common/task_manager.py
@@ -0,0 +1,104 @@
+# This file is part of Gajim.
+#
+# Gajim is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published
+# by the Free Software Foundation; version 3 only.
+#
+# Gajim is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import annotations
+
+from typing import List
+
+import functools
+import queue
+import logging
+
+from gi.repository import GLib
+
+log = logging.getLogger('gajim.c.m.task_manager')
+
+
+class TaskManager:
+    def __init__(self):
+        self._timeout = None
+        self._queue = queue.PriorityQueue()
+
+    def _start_worker(self):
+        self._timeout = GLib.timeout_add_seconds(2, self._process_queue)
+
+    def _process_queue(self):
+        log.info('%s tasks queued', self._queue.qsize())
+        requeue = []
+        while not self._queue.empty():
+            task = self._queue.get_nowait()
+            if task.is_obsolete():
+                log.info('Task obsolete: %r', task)
+                continue
+
+            if not task.preconditions_met():
+                if task.is_obsolete():
+                    log.info('Task obsolete: %r', task)
+                else:
+                    requeue.append(task)
+                continue
+
+            log.info('Execute task %r', task)
+            task.execute()
+            self._requeue_tasks(requeue)
+            return True
+
+        if self._requeue_tasks(requeue):
+            # Queue is empty, but there are tasks to requeue
+            # don't stop worker
+            return True
+
+        # Queue is empty, stop worker
+        self._timeout = None
+        return False
+
+    def _requeue_tasks(self, tasks: List[Task]):
+        if not tasks:
+            return False
+
+        for task in tasks:
+            log.info('Requeue task (preconditions not met): %r', task)
+            self._queue.put_nowait(task)
+        return True
+
+    def add_task(self, task: Task):
+        log.info('Adding task: %r', task)
+        self._queue.put_nowait(task)
+        if self._timeout is None:
+            self._start_worker()
+
+
+@functools.total_ordering
+class Task:
+    def __init__(self, priority: int = 0):
+        self.priority = priority
+        self._obsolete = False
+
+    def is_obsolete(self):
+        return self._obsolete
+
+    def set_obsolete(self):
+        self._obsolete = True
+
+    def __lt__(self, task: Task):
+        return self.priority < task.priority
+
+    def __eq__(self, task: Task):
+        return task.priority == self.priority
+
+    def execute(self):
+        raise NotImplementedError
+
+    def preconditions_met(self):
+        raise NotImplementedError