diff --git a/app/routes/Blubber.php b/app/routes/Blubber.php
index a30feb9a1ea100312a097d5c85f9969720cf7ed4..4fb6ab9d87ec4adf46e149e5080c271b639b4e46 100644
--- a/app/routes/Blubber.php
+++ b/app/routes/Blubber.php
@@ -18,7 +18,7 @@ class Blubber extends \RESTAPI\RouteMap
      *
      * @get /blubber/threads/:thread_id
      * @param string $thread_id   id of the blubber thread or "global" if you want public threads (not comments). Remind the global thread is a virtual thread with a special behaviour.
-     * @return Array   the blubber as array
+     * @return array   the blubber as array
      */
     public function getThreadData($thread_id)
     {
@@ -31,7 +31,6 @@ class Blubber extends \RESTAPI\RouteMap
         $thread = \BlubberThread::upgradeThread($thread);
         if (!$thread->isReadable()) {
             $this->error(401);
-            return;
         }
 
         $json = $thread->getJSONData(50, null, \Request::get("search"));
@@ -46,7 +45,7 @@ class Blubber extends \RESTAPI\RouteMap
      * Get threads
      *
      * @get /blubber/threads
-     * @return Array   the stream as array
+     * @return array   the stream as array
      */
     public function getMyThreads()
     {
@@ -83,7 +82,7 @@ class Blubber extends \RESTAPI\RouteMap
      *
      * @post /blubber/threads/:thread_id/comments
      * @param string $thread_id   id of the blubber thread
-     * @return Array   the comment as array
+     * @return array   the comment as array
      */
     public function postComment($thread_id)
     {
@@ -93,13 +92,11 @@ class Blubber extends \RESTAPI\RouteMap
 
         if (!trim($this->data['content'])) {
             $this->error(406);
-            return false;
         }
 
         $thread = \BlubberThread::find($thread_id);
         if (!$thread->isCommentable()) {
             $this->error(401);
-            return;
         }
 
         $comment = new \BlubberComment();
@@ -109,7 +106,7 @@ class Blubber extends \RESTAPI\RouteMap
         $comment['external_contact'] = 0;
         $comment->store();
 
-        $GLOBALS['user']->cfg->store("BLUBBERTHREAD_VISITED_".$thread_id, time());
+        $thread->setLastVisit();
 
         return $comment->getJSONData();
     }
@@ -122,14 +119,13 @@ class Blubber extends \RESTAPI\RouteMap
      * @param string $thread_id   id of the blubber thread
      * @param string $comment     id of the comment
      *
-     * @return Array   the comment as array
+     * @return array   the comment as array
      */
     public function editComment($thread_id, $comment_id)
     {
         $comment = \BlubberComment::find($comment_id);
         if (!$comment->isWritable()) {
             $this->error(401);
-            return;
         }
         $old_content = $comment['content'];
         $comment['content'] = $this->data['content'];
@@ -176,7 +172,7 @@ class Blubber extends \RESTAPI\RouteMap
      *
      * @param string $thread_id   id of the blubber thread
      *
-     * @return Array   the comments as array
+     * @return array   the comments as array
      */
     public function getComments($thread_id)
     {
@@ -187,7 +183,6 @@ class Blubber extends \RESTAPI\RouteMap
         $thread = new \BlubberThread($thread_id);
         if (!$thread->isReadable()) {
             $this->error(401);
-            return;
         }
 
         $modifier = \Request::get('modifier');
@@ -228,7 +223,7 @@ class Blubber extends \RESTAPI\RouteMap
                       FROM blubber_comments
                       WHERE blubber_comments.thread_id = :thread_id
                         AND blubber_comments.mkdate >= :timestamp
-                      ORDER BY mkdate ASC
+                      ORDER BY mkdate
                       LIMIT :limit";
             $comments = \DBManager::get()->fetchAll($query, [
                 'thread_id' => $thread_id,
diff --git a/db/migrations/5.1.37_migrate_blubber_user_config_to_object_user_visits.php b/db/migrations/5.1.37_migrate_blubber_user_config_to_object_user_visits.php
new file mode 100644
index 0000000000000000000000000000000000000000..69e7ea64b7a0a66ab5f19871cbe3b809da2cc41e
--- /dev/null
+++ b/db/migrations/5.1.37_migrate_blubber_user_config_to_object_user_visits.php
@@ -0,0 +1,66 @@
+<?php
+final class MigrateBlubberUserConfigToObjectUserVisits extends Migration
+{
+    public function description()
+    {
+        return 'Migrates the blubber visited entrires from config_values to user_entries';
+    }
+
+    protected function up()
+    {
+        $query = "SELECT `pluginid`
+                  FROM `plugins`
+                  WHERE `pluginclassname` = 'Blubber'";
+        $blubber_plugin_id = DBManager::get()->fetchColumn($query);
+
+        $query = "INSERT INTO `object_user_visits` (
+                     `object_id`,
+                     `user_id`,
+                     `plugin_id`,
+                     `visitdate`,
+                     `last_visitdate`
+                  )
+                  SELECT SUBSTR(`field`, 23) AS `object_id`,
+                     `range_id` AS `user_id`,
+                     ? AS `plugin_id`,
+                     `value` AS `visitdate`,
+                     `value` AS `last_visitdate`
+                  FROM `config_values`
+                  WHERE `field` LIKE 'BLUBBERTHREAD\\_VISITED\\_%'";
+        DBManager::get()->execute($query, [$blubber_plugin_id]);
+
+        $query = "DELETE FROM `config_values`
+                  WHERE `field` LIKE 'BLUBBERTHREAD\\_VISITED\\_%'";
+        DBManager::get()->exec($query);
+    }
+
+    protected function down()
+    {
+        $query = "SELECT `pluginid`
+                  FROM `plugins`
+                  WHERE `pluginclassname` = 'Blubber'";
+        $blubber_plugin_id = DBManager::get()->fetchColumn($query);
+
+        $query = "INSERT INTO `config_values` (
+                     `field`,
+                     `range_id`,
+                     `value`,
+                     `mkdate`,
+                     `chdate`,
+                     `comment`
+                  )
+                  SELECT CONCAT('BLUBBERTHREAD_VISITED_', `object_id`) AS `field`,
+                     `user_id` AS `range_id`,
+                     `visitdate` AS `value`,
+                     UNIX_TIMESTAMP() AS `mkdate`,
+                     UNIX_TIMESTAMP() AS `chdate`,
+                     '' AS `comment`
+                  FROM `object_user_visits`
+                  WHERE `plugin_id` = ?";
+        DBManager::get()->execute($query, [$blubber_plugin_id]);
+
+        $query = "DELETE FROM `object_user_visits`
+                  WHERE `plugin_id` = ?";
+        DBManager::get()->execute($query, [$blubber_plugin_id]);
+    }
+}
diff --git a/lib/classes/sidebar/BlubberThreadsWidget.php b/lib/classes/sidebar/BlubberThreadsWidget.php
index b02e027e2d14d14017e71d9ebe5ecdf837634198..db80023fa45bfb10e5b9b3bf362a157fe7aefe93 100644
--- a/lib/classes/sidebar/BlubberThreadsWidget.php
+++ b/lib/classes/sidebar/BlubberThreadsWidget.php
@@ -1,10 +1,16 @@
 <?php
 
+/**
+ * @property BlubberThread[] $elements
+ */
 class BlubberThreadsWidget extends SidebarWidget
 {
     protected $active_thread = null;
     protected $with_composer = false;
 
+    /**
+     * @param BlubberThread $thread
+     */
     public function addThread($thread)
     {
         $this->elements[] = $thread;
@@ -32,18 +38,18 @@ class BlubberThreadsWidget extends SidebarWidget
         foreach ($this->elements as $thread) {
             $unseen_comments = BlubberComment::countBySQL("thread_id = ? AND mkdate >= ?", [
                 $thread->getId(),
-                $thread->getLastVisit() ?: object_get_visit_threshold()
+                $thread->getLastVisit()
             ]);
 
             $json[] = [
-                'thread_id' => $thread->getId(),
-                'avatar' => $thread->getAvatar(),
-                'name' => $thread->getName(),
-                'timestamp' => (int) $thread->getLatestActivity(),
-                'mkdate' => (int) $thread->mkdate,
+                'thread_id'       => $thread->getId(),
+                'avatar'          => $thread->getAvatar(),
+                'name'            => $thread->getName(),
+                'timestamp'       => (int) $thread->getLatestActivity(),
+                'mkdate'          => (int) $thread->mkdate,
                 'unseen_comments' => $unseen_comments,
-                'notifications' => $thread->id === 'global' || ($thread->context_type === 'course' && !$GLOBALS['perm']->have_perm('admin')),
-                'followed' => $thread->isFollowedByUser(),
+                'notifications'   => $thread->id === 'global' || ($thread->context_type === 'course' && !$GLOBALS['perm']->have_perm('admin')),
+                'followed'        => $thread->isFollowedByUser(),
             ];
         }
 
diff --git a/lib/models/BlubberThread.php b/lib/models/BlubberThread.php
index 6c136b3bad7b545b3fb8a189299acb700601a5b8..ceb8a4b6bbe8096f01a54f2694f472f2549ff781 100644
--- a/lib/models/BlubberThread.php
+++ b/lib/models/BlubberThread.php
@@ -41,6 +41,11 @@ class BlubberThread extends SimpleORMap implements PrivacyObject
             'foreign_key'       => 'user_id',
             'assoc_foreign_key' => 'user_id',
         ];
+        $config['has_many']['visits'] = [
+            'class_name'        => ObjectUserVisit::class,
+            'assoc_foreign_key' => 'object_id',
+            'on_delete'         => 'delete',
+        ];
 
         $config['serialized_fields']['metadata'] = 'JSONArrayObject';
 
@@ -48,7 +53,6 @@ class BlubberThread extends SimpleORMap implements PrivacyObject
     }
 
     public static $mention_thread_id = null;
-    protected $last_visit = null;
 
     /**
      * Pre-Markup rule. Recognizes mentions in blubber as @username or @"Firstname lastname"
@@ -103,6 +107,9 @@ class BlubberThread extends SimpleORMap implements PrivacyObject
         return $markup->quote($matches[0]);
     }
 
+    /**
+     * @return BlubberThread[]
+     */
     public static function findBySQL($sql, $params = [])
     {
         return parent::findAndMapBySQL(function ($thread) {
@@ -110,6 +117,9 @@ class BlubberThread extends SimpleORMap implements PrivacyObject
         }, $sql, $params);
     }
 
+    /**
+     * @return BlubberThread|null
+     */
     public static function find($id)
     {
         return self::upgradeThread(parent::find($id));
@@ -606,7 +616,39 @@ class BlubberThread extends SimpleORMap implements PrivacyObject
      */
     public function getLastVisit(string $user_id = null)
     {
-        return UserConfig::get($user_id ?? $GLOBALS['user']->id)->getValue("BLUBBERTHREAD_VISITED_".$this->getId());
+        return object_get_visit(
+            $this->id,
+            $this->getBlubberPluginId(),
+            '',
+            '',
+            $user_id ?? User::findCurrent()->id
+        );
+    }
+
+    /**
+     * Sets the last visit timestamp for this thread
+     *
+     * @param string|null $user_id
+     */
+    public function setLastVisit(string $user_id = null): void
+    {
+        object_set_visit(
+            $this->id,
+            $this->getBlubberPluginId(),
+            $user_id ?? User::findCurrent()->id
+        );
+    }
+
+    /**
+     * Returns the id of the blubber plugin.
+     *
+     * @return int Id of the plugin
+     */
+    protected function getBlubberPluginId(): int
+    {
+        $plugin_info = PluginManager::getInstance()->getPluginInfo(Blubber::class);
+        return (int) $plugin_info['id'];
+
     }
 
     public function notifyUsersForNewComment($comment)
@@ -851,7 +893,7 @@ class BlubberThread extends SimpleORMap implements PrivacyObject
             'more_down'       => 0,
             'unseen_comments' => BlubberComment::countBySQL("thread_id = ? AND mkdate >= ? AND user_id != ?", [
                 $this->getId(),
-                $this->getLastVisit() ?: object_get_visit_threshold(),
+                $this->getLastVisit(),
                 $user_id
             ]),
             'notifications' => $this->mayDisableNotifications(),
@@ -927,10 +969,8 @@ class BlubberThread extends SimpleORMap implements PrivacyObject
             'user_id' => $user_id,
             'html_id' => "blubberthread_".$this->getId()
         ]);
-        $this->last_visit[$user_id] = !$this->last_visit[$user_id]
-            ? object_get_visit($this->getId(), "blubberthread", "last", "", $user_id)
-            : $this->last_visit[$user_id];
-        UserConfig::get($user_id)->store("BLUBBERTHREAD_VISITED_".$this->getId(), time());
+
+        $this->setLastVisit($user_id);
     }
 
     public function getHashtags($since = null)
diff --git a/lib/models/ObjectUserVisit.php b/lib/models/ObjectUserVisit.php
new file mode 100644
index 0000000000000000000000000000000000000000..90a331e042a2a3e7f05f2664462d95901b1bf889
--- /dev/null
+++ b/lib/models/ObjectUserVisit.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @property array $id
+ * @property string $object_id
+ * @property string $user_id
+ * @property int $plugin_id
+ * @property int $visitdate
+ * @property int $last_visitdate
+ * @property User $user
+ */
+class ObjectUserVisit extends SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'object_user_visits';
+
+        $config['belongs_to'] = [
+            'user' => [
+                'class_name'        => User::class,
+                'foreign_key'       => 'user_id',
+                'assoc_foreign_key' => 'user_id',
+            ]
+        ];
+
+        parent::configure($config);
+    }
+}
diff --git a/public/plugins_packages/core/Blubber/Blubber.class.php b/public/plugins_packages/core/Blubber/Blubber.class.php
index ce317ad24ba1e0d3c6c0411f28e640ee9995d721..65657921cdb6e3c423fb65b5168cf88d6e95c8e0 100644
--- a/public/plugins_packages/core/Blubber/Blubber.class.php
+++ b/public/plugins_packages/core/Blubber/Blubber.class.php
@@ -67,7 +67,11 @@ class Blubber extends StudIPPlugin implements StandardPlugin
             'me'         => $user_id,
         ]);
         foreach ($comments as $comment) {
-            if ($comment->thread->isVisibleInStream() && $comment->thread->isReadable() && ($comment->thread->getLatestActivity() > UserConfig::get($user_id)->getValue("BLUBBERTHREAD_VISITED_".$comment['thread_id']))) {
+            if (
+                $comment->thread->isVisibleInStream()
+                && $comment->thread->isReadable()
+                && $comment->thread->getLatestActivity() > $comment->thread->getLastVisit()
+            ) {
                 $icon->setImage(Icon::create('blubber', Icon::ROLE_NEW, ['title' => _('Es gibt neue Blubber')]));
                 $icon->setTitle(_('Es gibt neue Blubber'));
                 $icon->setBadgeNumber(count($comments));
@@ -91,7 +95,11 @@ class Blubber extends StudIPPlugin implements StandardPlugin
             'me'         => $GLOBALS['user']->id,
         ]);
         foreach ($threads as $thread) {
-            if ($thread->isVisibleInStream() && $thread->isReadable() && ($thread['mkdate'] > UserConfig::get($user_id)->getValue("BLUBBERTHREAD_VISITED_".$thread->getId()))) {
+            if (
+                $thread->isVisibleInStream()
+                && $thread->isReadable()
+                && $thread->mkdate > $thread->getLastVisit()
+            ) {
                 $icon->setImage(Icon::create('blubber', Icon::ROLE_ATTENTION, ['title' => _('Es gibt neue Blubber')]));
                 $icon->setTitle(_('Es gibt neue Blubber'));
                 break;