diff --git a/.python-version b/.python-version
index 3e72aa69867656147f94eadbb5405bcc33adcc7c..2d4715b6a211083d26526ff357cd6b9a42fe16f7 100644
--- a/.python-version
+++ b/.python-version
@@ -1 +1 @@
-3.11.10
+3.11.11
diff --git a/anon_stats/__init__.py b/anon_stats/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/anon_stats/apps.py b/anon_stats/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ec723af6a8aefe6c595df8471f4782413019376
--- /dev/null
+++ b/anon_stats/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class AnonStatsConfig(AppConfig):
+    name = "anon_stats"
diff --git a/anon_stats/migrations/0001_initial.py b/anon_stats/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b61162e2394a8699a3dc40f67548260ac05cc3d
--- /dev/null
+++ b/anon_stats/migrations/0001_initial.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.0.10 on 2025-01-31 08:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = []
+
+    operations = [
+        migrations.CreateModel(
+            name="Counter",
+            fields=[
+                ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+                ("name", models.CharField(max_length=255)),
+                ("timestamp", models.DateTimeField(auto_now_add=True)),
+            ],
+        ),
+    ]
diff --git a/anon_stats/migrations/__init__.py b/anon_stats/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/anon_stats/models.py b/anon_stats/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..e4ba262e5e9b4f8afa31dee162a308fed22c97da
--- /dev/null
+++ b/anon_stats/models.py
@@ -0,0 +1,10 @@
+from datetime import datetime
+
+from django.db import models
+
+
+# super simple model for a counter to simply insert a row with a counter name and timestamp
+# to evaluate the counters value, a query needs to count the number of rows for the whole or a specific time frame
+class Counter(models.Model):
+    name: str = models.CharField(max_length=255)
+    timestamp: datetime = models.DateTimeField(auto_now_add=True)
diff --git a/anon_stats/schema/__init__.py b/anon_stats/schema/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/anon_stats/schema/mutations.py b/anon_stats/schema/mutations.py
new file mode 100644
index 0000000000000000000000000000000000000000..7fbcd27a22554ec2d75cfbce8f8f28a7322ecd3c
--- /dev/null
+++ b/anon_stats/schema/mutations.py
@@ -0,0 +1,26 @@
+from datetime import datetime
+from logging import Logger
+
+import strawberry
+import structlog
+
+from anon_stats.models import Counter
+
+logger: Logger = structlog.get_logger(__name__)
+
+
+# Create your tests here.
+@strawberry.type
+class Mutation:
+    @strawberry.mutation()
+    async def anon_count(self, counter_name: str) -> None:
+        # silently ignore if no counter_name was given
+        if not counter_name or counter_name.strip() == "":
+            return
+        # caller does not care about the result so we ignore any potential errors that could occur
+        # noinspection PyAsyncCall
+        try:
+            await Counter.objects.acreate(name=counter_name, timestamp=datetime.now())
+        except Exception as e:
+            logger.warning(f"Silently ignoring exception while creating counter {e}", exc_info=True)
+            pass
diff --git a/anon_stats/tests/__init__.py b/anon_stats/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/anon_stats/tests/test_mutations.py b/anon_stats/tests/test_mutations.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e7503272ca44316bbe389d6a6d160eefb195b82
--- /dev/null
+++ b/anon_stats/tests/test_mutations.py
@@ -0,0 +1,58 @@
+import pytest
+from asgiref.sync import async_to_sync
+
+from anon_stats.models import Counter
+from openbook.graphql_schema import schema
+from unittest import mock
+
+
+@pytest.mark.django_db
+class TestAnonCountMutation:
+    @staticmethod
+    def _increment_count(counter_name):
+        mutation = f"""
+            mutation {{
+                anonCount(counterName: "{counter_name}")
+            }}
+        """
+
+        response = async_to_sync(schema.execute)(mutation)
+        assert response.errors is None
+
+    @staticmethod
+    def _validate_counts(counter_name, expected_count):
+        counters = Counter.objects.filter(name=counter_name)
+        assert counters.exists() == (expected_count > 0)
+        assert counters.count() == expected_count, "Unexpected number of counts created"
+        for counter in counters:
+            assert counter.name == counter_name, "Counter name mismatch"
+
+    def test_anon_count_creates_counter(self):
+        self._increment_count("test_counter")
+        self._validate_counts("test_counter", 1)
+
+        self._increment_count("test_counter")
+        self._validate_counts("test_counter", 2)
+
+    def test_anon_count_supports_multiple_counters(self):
+        self._increment_count("test_counter")
+        self._increment_count("other_test_counter")
+        self._validate_counts("test_counter", 1)
+        self._validate_counts("other_test_counter", 1)
+
+        self._increment_count("other_test_counter")
+        self._validate_counts("other_test_counter", 2)
+
+    def test_anon_count_empty_name(self):
+        self._increment_count(" ")
+        self._validate_counts("", 0)
+        self._validate_counts(" ", 0)
+
+    @mock.patch("anon_stats.models.Counter.objects.acreate")
+    def test_anon_count_handles_database_exceptions(self, mock_acreate):
+        mock_acreate.side_effect = Exception("Database error")
+
+        self._increment_count("test_counter")
+
+        counters = Counter.objects.filter(name="test_counter")
+        assert not counters.exists(), "Counter should not be created when there is a database exception"
diff --git a/feed_posts/schema/feed_queries.py b/feed_posts/schema/feed_queries.py
index a4cd02e5cc68e74b71d879a7002e4e6d551e4cb9..2a876ab640fdfd7ca993be0aac83cac977db0394 100644
--- a/feed_posts/schema/feed_queries.py
+++ b/feed_posts/schema/feed_queries.py
@@ -85,6 +85,7 @@ class Query:
                 reaction_count=Count("reactions"),
                 total_comments=Subquery(space_comments_subquery, output_field=models.IntegerField()),
             )
+            .select_related("community")
             .order_by("-created", "-reaction_count", "-total_comments")
             .distinct()
         )
@@ -119,99 +120,163 @@ class Query:
                 reaction_count=Count("reactions"),
                 total_comments=Coalesce(Subquery(feed_post_comments_subquery, output_field=models.IntegerField()), 0),
             )
+            .select_related("creator")
+            .prefetch_related("topics")
             .order_by("-created_at", "-reaction_count", "-total_comments")
             .all()
         )
 
-        user_interests = list(user.profile.interests.values_list("id", flat=True).all())
-
+        user_interests = list(user.profile.interests.values_list("id", flat=True))
         user_connections = list(user.connections.values_list("target_user_id", flat=True))
-
         current_time = timezone.now()
 
-        # User recently created posts
-        recently_created_user_feed_posts = feed_posts.filter(
-            creator=user, created_at__gte=current_time - timedelta(hours=1)
-        ).all()
+        # Execute all queries in parallel while preserving order
+        from concurrent.futures import ThreadPoolExecutor
 
-        # User recently created space posts
-        recently_created_user_space_posts = (
-            PostModel.objects.filter(
-                creator=user, status=PostModel.STATUS_PUBLISHED, created__gte=current_time - timedelta(hours=1)
+        with ThreadPoolExecutor(max_workers=4) as executor:
+            futures = []
+
+            # User recently created posts
+            futures.append(
+                executor.submit(
+                    lambda: list(
+                        feed_posts.filter(creator=user, created_at__gte=current_time - timedelta(hours=1)).all()
+                    )
+                )
             )
-            .order_by("-created")
-            .distinct()
-        )
 
-        # Co-founders
-        co_founder_feed_posts = [post for post in feed_posts if post.has_priority]
-
-        # Editors priority posts
-        editors_priority_feed_posts = [
-            post for post in feed_posts if post.created_at >= current_time - timedelta(days=post.priority_duration)
-        ]
-
-        # User connections posts
-        user_connections_feed_posts = feed_posts.filter(
-            creator__id__in=user_connections, created_at__gte=current_time - priority_duration
-        ).all()
-
-        # Space posts in spaces that the user is collaborator
-        space_posts_user_is_contributor = space_posts.filter(
-            community__memberships__user=user, created__gte=current_time - priority_duration
-        ).all()
-
-        space_posts_user_is_follower = space_posts.filter(
-            community__notifications_subscriptions__subscriber=user,
-            community__notifications_subscriptions__new_post_notifications=True,
-            created__gte=current_time - priority_duration,
-        ).all()
-
-        # Feed posts matching user's topics
-        matching_interest_feed_posts = feed_posts.filter(
-            topics__id__in=user_interests, created_at__gte=current_time - priority_duration
-        ).all()
-
-        # Space posts matching user's topics
-        matching_interest_space_posts = space_posts.filter(
-            community__topics__id__in=user_interests, created__gte=current_time - priority_duration
-        ).all()
-
-        # Editors posts
-        editors_feed_posts = feed_posts.filter(
-            creator__role=UserRole.EDITOR, created_at__gte=current_time - priority_duration
-        ).all()
-
-        # Latest feed posts
-        latest_feed_posts = feed_posts.filter(creator=user, created_at__gte=current_time - timedelta(days=1)).all()
-
-        # Latest space posts
-        latest_space_posts = (
-            PostModel.objects.filter(
-                creator=user, status=PostModel.STATUS_PUBLISHED, created__gte=current_time - timedelta(days=1)
+            # User recently created space posts
+            futures.append(
+                executor.submit(
+                    lambda: list(
+                        PostModel.objects.filter(
+                            creator=user,
+                            status=PostModel.STATUS_PUBLISHED,
+                            created__gte=current_time - timedelta(hours=1),
+                        )
+                        .order_by("-created")
+                        .distinct()
+                    )
+                )
             )
-            .order_by("-created")
-            .distinct()
-        )
 
-        all_posts = OrderedDict()
+            # Co-founders
+            futures.append(executor.submit(lambda: [post for post in feed_posts if post.has_priority]))
+
+            # Editors priority posts
+            futures.append(
+                executor.submit(
+                    lambda: [
+                        post
+                        for post in feed_posts
+                        if post.created_at >= current_time - timedelta(days=post.priority_duration)
+                    ]
+                )
+            )
+
+            # User connections posts
+            futures.append(
+                executor.submit(
+                    lambda: list(
+                        feed_posts.filter(
+                            creator__id__in=user_connections, created_at__gte=current_time - priority_duration
+                        ).all()
+                    )
+                )
+            )
+
+            # Space posts in spaces that the user is collaborator
+            futures.append(
+                executor.submit(
+                    lambda: list(
+                        space_posts.filter(
+                            community__memberships__user=user, created__gte=current_time - priority_duration
+                        ).all()
+                    )
+                )
+            )
+
+            # Space posts user is follower of
+            futures.append(
+                executor.submit(
+                    lambda: list(
+                        space_posts.filter(
+                            community__notifications_subscriptions__subscriber=user,
+                            community__notifications_subscriptions__new_post_notifications=True,
+                            created__gte=current_time - priority_duration,
+                        ).all()
+                    )
+                )
+            )
+
+            # Feed posts matching user's topics
+            futures.append(
+                executor.submit(
+                    lambda: list(
+                        feed_posts.filter(
+                            topics__id__in=user_interests, created_at__gte=current_time - priority_duration
+                        ).all()
+                    )
+                )
+            )
+
+            # Space posts matching user's topics
+            futures.append(
+                executor.submit(
+                    lambda: list(
+                        space_posts.filter(
+                            community__topics__id__in=user_interests, created__gte=current_time - priority_duration
+                        ).all()
+                    )
+                )
+            )
+
+            # Editors posts
+            futures.append(
+                executor.submit(
+                    lambda: list(
+                        feed_posts.filter(
+                            creator__role=UserRole.EDITOR, created_at__gte=current_time - priority_duration
+                        ).all()
+                    )
+                )
+            )
 
-        for post_list in [
-            recently_created_user_feed_posts,
-            recently_created_user_space_posts,
-            co_founder_feed_posts,
-            editors_priority_feed_posts,
-            user_connections_feed_posts,
-            space_posts_user_is_contributor,
-            space_posts_user_is_follower,
-            matching_interest_feed_posts,
-            matching_interest_space_posts,
-            editors_feed_posts,
-            latest_feed_posts,
-            latest_space_posts,
-            feed_posts,
-            space_posts,
-        ]:
+            # Latest feed posts
+            futures.append(
+                executor.submit(
+                    lambda: list(
+                        feed_posts.filter(creator=user, created_at__gte=current_time - timedelta(days=1)).all()
+                    )
+                )
+            )
+
+            # Latest space posts
+            futures.append(
+                executor.submit(
+                    lambda: list(
+                        PostModel.objects.filter(
+                            creator=user,
+                            status=PostModel.STATUS_PUBLISHED,
+                            created__gte=current_time - timedelta(days=1),
+                        )
+                        .order_by("-created")
+                        .distinct()
+                    )
+                )
+            )
+
+            # All feed posts
+            futures.append(executor.submit(lambda: list(feed_posts)))
+
+            # All space posts
+            futures.append(executor.submit(lambda: list(space_posts)))
+
+            # Get results in order
+            post_lists = [future.result() for future in futures]
+
+        all_posts = OrderedDict()
+        for post_list in post_lists:
             for post in post_list:
                 all_posts[post] = None
 
diff --git a/openbook/graphql_schema.py b/openbook/graphql_schema.py
index f70dae14f77ebd74d03cf2391cae05adf1589dcf..8ef282116cd0526dea4aa374e3625c883009d77f 100644
--- a/openbook/graphql_schema.py
+++ b/openbook/graphql_schema.py
@@ -16,6 +16,7 @@ from openbook.middleware.graphql_error_extension import GraphQLErrorExtension
 from openbook.settings.sentry_config import HoliSentryTracingExtension
 from openbook_appointments.schema import mutations as appointments_mutations
 from openbook_appointments.schema import queries as appointments_queries
+from anon_stats.schema import mutations as anon_stats
 from openbook_auth.schema import mutations as auth_mutations
 from openbook_auth.schema import queries as auth_queries
 from openbook_common.schema import queries as commons_queries
@@ -89,6 +90,7 @@ Query = merge_types(
 Mutation = merge_types(
     "Mutation",
     (
+        anon_stats.Mutation,
         auth_mutations.Mutation,
         connections_mutations.Mutation,
         posts_mutations.Mutation,
diff --git a/openbook/settings/__init__.py b/openbook/settings/__init__.py
index bd847c47fab6977619802824e0121100e5eef786..c96871d447ce9aa12cfb92276fdcee9eadf50355 100644
--- a/openbook/settings/__init__.py
+++ b/openbook/settings/__init__.py
@@ -89,6 +89,7 @@ INSTALLED_APPS = [
     "rangefilter",
     "django_rq",
     "django_extensions",
+    "anon_stats",
     "openbook_common",
     "openbook_auth",
     "openbook_posts",
@@ -517,8 +518,6 @@ PROFILE_GEOLOCATION_ID_MAX_LENGTH = 255
 PROFILE_ABOUT_ME_MAX_LENGTH = int(os.environ.get("PROFILE_ABOUT_ME_MAX_LENGTH", "3000"))
 PROFILE_AVATAR_MAX_SIZE = int(os.environ.get("PROFILE_AVATAR_MAX_SIZE", "10485760"))
 PROFILE_AVATAR_URL_MAX_LENGTH = 500
-PROFILE_COVER_MAX_SIZE = int(os.environ.get("PROFILE_COVER_MAX_SIZE", "10485760"))
-PROFILE_COVER_URL_MAX_LENGTH = 500
 PASSWORD_RESET_TIMEOUT_DAYS = 1
 COMMUNITY_NAME_MAX_LENGTH = 80
 COMMUNITY_TITLE_MIN_LENGTH = 2
diff --git a/openbook_auth/forms.py b/openbook_auth/forms.py
index 14b537ed64ad341dc77398e4876744b3f75ce401..71f5cd1fd87a741dfa55e96bbd751faee4d8b32f 100644
--- a/openbook_auth/forms.py
+++ b/openbook_auth/forms.py
@@ -6,7 +6,6 @@ class UserProfileFormSet(BaseInlineFormSet):
         super(UserProfileFormSet, self).__init__(*args, **kwargs)
         for form in self.forms:
             form.fields["avatar"].required = False
-            form.fields["cover"].required = False
             form.fields["badges"].required = False
             form.fields["url"].required = False
             form.fields["engagement_level"].required = False
diff --git a/openbook_auth/helpers.py b/openbook_auth/helpers.py
index 86a6d2961509749745b016aab024950b95eee3cb..352bf444eee306cda860833a1b33055bebfd4e8b 100644
--- a/openbook_auth/helpers.py
+++ b/openbook_auth/helpers.py
@@ -11,9 +11,4 @@ def upload_to_user_avatar_directory(user_profile, filename):
 
 
 def upload_to_user_cover_directory(user_profile, filename):
-    return create_upload_filename(
-        username=user_profile.user.username,
-        object_type="user_cover",
-        object_id=user_profile.user.username,
-        filename=filename,
-    )
+    pass  # removed functionality still referenced in old migrations
diff --git a/openbook_auth/migrations/0034_remove_userprofile_cover_and_more.py b/openbook_auth/migrations/0034_remove_userprofile_cover_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..ba112d8261ed6d90f478fc32fd0bdd79456ea6dc
--- /dev/null
+++ b/openbook_auth/migrations/0034_remove_userprofile_cover_and_more.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.0.10 on 2025-01-30 09:35
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("openbook_auth", "0033_alter_user_email"),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name="userprofile",
+            name="cover",
+        ),
+        migrations.RemoveField(
+            model_name="userprofile",
+            name="cover_blurhash",
+        ),
+    ]
diff --git a/openbook_auth/models.py b/openbook_auth/models.py
index 38584889cd3d2f379c20bd5be89f490a1a9c9eda..905f06fe21099b34a348bc16af56f0fde5c9235a 100644
--- a/openbook_auth/models.py
+++ b/openbook_auth/models.py
@@ -32,10 +32,7 @@ from openbook_common.tracking import track, TrackingEvent
 from openbook.settings import USERBLOCK
 from openbook.utils import DEFAULT_EMAIL_LANGUAGE, get_supported_language_for_email
 from openbook_auth.checkers import *
-from openbook_auth.helpers import (
-    upload_to_user_avatar_directory,
-    upload_to_user_cover_directory,
-)
+from openbook_auth.helpers import upload_to_user_avatar_directory
 from openbook_auth.utils import normalize_spaces
 from openbook_common.enums import VisibilityType
 from openbook_common.helpers import ExifRotate, generate_blurhash, PrefixedQ
@@ -647,22 +644,6 @@ class User(ModelWithUUID, AbstractUser):
         self.is_deleted = False
         self.save()
 
-    def _update_profile_cover(self, cover, save=True):
-        if cover is None:
-            self._delete_profile_cover(save=False)
-        else:
-            self.profile.cover = cover
-            self.profile.cover_blurhash = generate_blurhash(cover)
-
-        if save:
-            self.profile.save()
-
-    def _delete_profile_cover(self, save=True):
-        delete_file_field(self.profile.cover)
-        self.profile.cover = None
-        self.profile.cover_blurhash = None
-        self.profile.cover.delete(save=save)
-
     def _update_profile_avatar(self, avatar, save=True):
         if self.profile.avatar:
             self._delete_profile_avatar(save=False)
@@ -678,11 +659,6 @@ class User(ModelWithUUID, AbstractUser):
         self.profile.avatar_blurhash = None
         self.profile.avatar.delete(save=save)
 
-    def update_username(self, username):
-        check_username_not_taken(user=self, username=username)
-        self.username = username
-        self.save()
-
     def update_password(self, password):
         self.set_password(password)
         self._reset_auth_token()
@@ -694,11 +670,6 @@ class User(ModelWithUUID, AbstractUser):
         verify_token = self._make_email_verification_token_for_email(new_email=email)
         return verify_token
 
-    # engagement_level may only be set once
-    def update_engagement_level(self, engagement_level):
-        self.profile.engagement_level = engagement_level
-        self.profile.save()
-
     def verify_email_with_token(self, token):
         new_email = check_email_verification_token_is_valid_for_email(user=self, email_verification_token=token)
         self.email = new_email
@@ -805,7 +776,6 @@ class User(ModelWithUUID, AbstractUser):
 
     def update(
         self,
-        username=None,
         name=None,
         location=None,
         geolocation=None,
@@ -815,8 +785,6 @@ class User(ModelWithUUID, AbstractUser):
         community_posts_visible=None,
         visibility=None,
         avatar=None,
-        cover=None,
-        remove_cover=False,
         last_name=None,
         pronouns=None,
         engagement_level=None,
@@ -828,42 +796,39 @@ class User(ModelWithUUID, AbstractUser):
         remove_avatar=False,
         tracking_consent_analytics=None,
         tracking_consent_personalization=None,
-    ):
-        profile = self.profile
-
-        if username:
-            self.update_username(username)
-
+    ) -> None:
         if url is not None:
             if len(url) == 0:
-                profile.url = None
+                self.profile.url = None
             else:
-                profile.url = url
+                self.profile.url = url
 
         if name:
-            profile.name = normalize_spaces(name)
+            self.profile.name = normalize_spaces(name)
+        if last_name is not None:
+            self.profile.last_name = normalize_spaces(last_name)
 
         if update_location:
             if geolocation:
-                profile.location = location or geolocation.name
-                profile.geolocation = geolocation.geometry
-                profile.geolocation_id = geolocation.id
+                self.profile.location = location or geolocation.name
+                self.profile.geolocation = geolocation.geometry
+                self.profile.geolocation_id = geolocation.id
             else:
-                profile.location = location
-                profile.geolocation = None
-                profile.geolocation_id = None
+                self.profile.location = location
+                self.profile.geolocation = None
+                self.profile.geolocation_id = None
 
         if about_me is not None:
             if len(about_me) == 0:
-                profile.about_me = None
+                self.profile.about_me = None
             else:
-                profile.about_me = about_me
+                self.profile.about_me = about_me
 
         if followers_count_visible is not None:
-            profile.followers_count_visible = followers_count_visible
+            self.profile.followers_count_visible = followers_count_visible
 
         if community_posts_visible is not None:
-            profile.community_posts_visible = community_posts_visible
+            self.profile.community_posts_visible = community_posts_visible
 
         if visibility:
             if self.visibility == User.VISIBILITY_TYPE_PRIVATE and visibility != User.VISIBILITY_TYPE_PRIVATE:
@@ -881,31 +846,24 @@ class User(ModelWithUUID, AbstractUser):
         if tracking_consent_personalization is not None:
             self.tracking_consent_personalization = tracking_consent_personalization
 
-        if last_name is not None:
-            profile.last_name = normalize_spaces(last_name)
         if pronouns is not None:
-            profile.pronouns = pronouns
+            self.profile.pronouns = pronouns
         if engagement_level:
-            self.update_engagement_level(engagement_level)
+            self.profile.engagement_level = engagement_level
         if interests is not None:
-            profile.interests.set(interests)
+            self.profile.interests.set(interests)
         if sdgs is not None:
-            profile.sdgs.set(sdgs)
+            self.profile.sdgs.set(sdgs)
         if skills is not None:
-            profile.skills.set(skills)
+            self.profile.skills.set(skills)
 
         if remove_avatar:
             self._delete_profile_avatar(save=False)
         elif avatar:
             self._update_profile_avatar(avatar, save=False)
 
-        if remove_cover:
-            self._delete_profile_cover(save=False)
-        elif cover:
-            self._update_profile_cover(cover, save=False)
-
         if save:
-            profile.save()
+            self.profile.save()
             self.save()
 
     def update_notifications_settings(
@@ -5259,22 +5217,6 @@ class UserProfile(ModelWithUUID):
         max_length=settings.IMAGE_URL_MAX_LENGTH,
     )
     avatar_blurhash = models.CharField(max_length=50, blank=True, null=True)
-    cover = ProcessedImageField(
-        verbose_name=_("cover"),
-        blank=False,
-        null=True,
-        upload_to=upload_to_user_cover_directory,
-        processors=[
-            ExifRotate(),
-            ResizeToFit(
-                width=settings.CONTENT_IMAGE_MAX_DIMENSION,
-                height=settings.CONTENT_IMAGE_MAX_DIMENSION,
-                upscale=False,
-            ),
-        ],
-        max_length=settings.IMAGE_URL_MAX_LENGTH,
-    )
-    cover_blurhash = models.CharField(max_length=50, blank=True, null=True)
     about_me = models.TextField(
         _("about me"),
         max_length=settings.PROFILE_ABOUT_ME_MAX_LENGTH,
diff --git a/openbook_auth/schema/mutations.py b/openbook_auth/schema/mutations.py
index 024c217eb5cde308ae475da17f32aa1d5c30b056..69daf5bd9b40a53b24b164806ba28cd7313f536a 100644
--- a/openbook_auth/schema/mutations.py
+++ b/openbook_auth/schema/mutations.py
@@ -2,6 +2,7 @@
 import uuid
 from copy import deepcopy
 from datetime import datetime, timedelta
+from typing import Dict, List
 
 import requests
 import strawberry
@@ -43,6 +44,28 @@ from openbook_notifications.services import NotificationsService
 logger = structlog.get_logger(__name__)
 
 
+def _capture_user_fields(user: UserModel) -> Dict[str, str]:
+    # for many-to-many relations (interests, sdgs, skills), only capture ids to avoid db round-trips
+    return {
+        "email": user.email,
+        "firstName": user.profile.name,
+        "lastName": user.profile.last_name,
+        "location": user.profile.location,
+        "geolocation": user.profile.geolocation,
+        "aboutMe": user.profile.about_me,
+        "pronouns": user.profile.pronouns,
+        "engagementLevel": user.profile.engagement_level,
+        "interests": set(user.profile.interests.values_list("id", flat=True)),
+        "sdgs": set(user.profile.sdgs.values_list("id", flat=True)),
+        "skills": set(user.profile.skills.values_list("id", flat=True)),
+        "avatar": user.profile.avatar.url if user.profile.avatar else None,
+    }
+
+
+def _get_updated_fields(o1: Dict[str, any], o2: Dict[str, any]) -> List[str]:
+    return [key for key in set(o1.keys()).union(o2.keys()) if o1.get(key) != o2.get(key)]
+
+
 @strawberry.type
 class Mutation:
     # TODO HOLI-8088 remove once not used by the frontend anymore
@@ -71,6 +94,7 @@ class Mutation:
         data = serializer.validated_data
 
         remove_avatar = is_parameter_null(data, "avatar")
+        current_user_fields_before = _capture_user_fields(current_user)
 
         current_user.update(
             name=data.get("name"),
@@ -90,6 +114,7 @@ class Mutation:
             tracking_consent_personalization=data.get("tracking_consent_personalization"),
             save=True,
         )
+        current_user_fields_after = _capture_user_fields(current_user)
 
         generate_identity_when_needed(current_user)
 
@@ -148,7 +173,9 @@ class Mutation:
                 user=current_user, spaces=spaces, insights=insights
             )
 
-        track(current_user, TrackingEvent("userUpdated"))
+        updated_fields = _get_updated_fields(current_user_fields_before, current_user_fields_after)
+        if updated_fields:
+            track(current_user, TrackingEvent("userUpdated", {"updatedFields": updated_fields}))
 
         cache.set(f"{settings.USER_CACHE_PREFIX}{current_user.username}", current_user, timeout=300)
 
diff --git a/openbook_auth/schema/queries.py b/openbook_auth/schema/queries.py
index 3adb8ea4be36151d65566523a39225570003bb84..59953ff6e04da48630997945d41e9cf9da3ac7c0 100644
--- a/openbook_auth/schema/queries.py
+++ b/openbook_auth/schema/queries.py
@@ -125,7 +125,7 @@ class Query:
     @strawberry_django.field()
     def connections_by_user_id(self, info, user_id: uuid.UUID, offset: int = 0, limit: int = 10) -> Paged[User]:
         verify_authorized_user(info)
-        user = UserModel.objects.get(id=user_id)
+        user = UserModel.objects.get(username=user_id)
         return get_ordered_user_connections(circle_id=user.connections_circle_id, offset=offset, limit=limit)
 
     @strawberry_django.field(
diff --git a/openbook_auth/serializers/authenticated_user_serializers.py b/openbook_auth/serializers/authenticated_user_serializers.py
index 800da24ec1be778c8039dd98c0021f9fb2da20c8..916dca8a17fffd90da1d81ef70e57b57ebe7f73f 100644
--- a/openbook_auth/serializers/authenticated_user_serializers.py
+++ b/openbook_auth/serializers/authenticated_user_serializers.py
@@ -37,12 +37,6 @@ class UpdateAuthenticatedUserSerializer(serializers.Serializer):
         allow_null=True,
         max_upload_size=settings.PROFILE_AVATAR_MAX_SIZE,
     )
-    cover = RestrictedImageFileSizeField(
-        allow_empty_file=False,
-        required=False,
-        allow_null=True,
-        max_upload_size=settings.PROFILE_COVER_MAX_SIZE,
-    )
     password = serializers.CharField(
         min_length=PASSWORD_MIN_LENGTH,
         max_length=PASSWORD_MAX_LENGTH,
diff --git a/openbook_auth/serializers/users_serializers.py b/openbook_auth/serializers/users_serializers.py
index 11c4232d4335a910fee93c5f5641dd04e5a5d033..58665b9188c75751e0839739f14ef13f8a4d37cd 100644
--- a/openbook_auth/serializers/users_serializers.py
+++ b/openbook_auth/serializers/users_serializers.py
@@ -59,7 +59,7 @@ class GetUserUserProfileSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = UserProfile
-        fields = ("name", "avatar", "location", "cover", "about_me", "url", "badges")
+        fields = ("name", "avatar", "location", "about_me", "url", "badges")
 
 
 class GetUserUserCircleSerializer(serializers.ModelSerializer):
diff --git a/openbook_auth/tests/media/.gitignore b/openbook_auth/tests/media/.gitignore
deleted file mode 100644
index 86d0cb2726c6c7c179b99520c452dd1b68e7a813..0000000000000000000000000000000000000000
--- a/openbook_auth/tests/media/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-# Ignore everything in this directory
-*
-# Except this file
-!.gitignore
\ No newline at end of file
diff --git a/openbook_auth/tests/test_avatar.jpg b/openbook_auth/tests/test_avatar.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..9ee90f64c2be850e5ce23020fff92a0e95bfd7e5
Binary files /dev/null and b/openbook_auth/tests/test_avatar.jpg differ
diff --git a/openbook_auth/tests/test_graphql.py b/openbook_auth/tests/test_graphql.py
index 73c4af7384051766920f9b9d7e8f0bbe20c62edc..df86032a0a14a9009083598f67788808c802ddde 100644
--- a/openbook_auth/tests/test_graphql.py
+++ b/openbook_auth/tests/test_graphql.py
@@ -1,10 +1,10 @@
 # ruff: noqa
-
+import os
 import json
 import logging
-import sys
 import uuid
 from datetime import datetime, timedelta
+from typing import Dict
 from unittest import mock
 from unittest.mock import call
 
@@ -15,6 +15,7 @@ from django.conf import settings
 from django.contrib.auth.models import AnonymousUser
 from django.contrib.gis import geos
 from django.core.management import call_command
+from django.core.files.uploadedfile import SimpleUploadedFile
 from django.http import HttpRequest
 from django.test import AsyncClient
 from django.urls import reverse
@@ -46,6 +47,9 @@ from openbook_common.tests.helpers import (
     make_user,
     assert_paged_results_equal,
     assert_is_forbidden,
+    make_topic,
+    make_sdg,
+    make_skill,
 )
 from openbook_common.tracking import TrackingEvent
 from openbook_communities.enums import UpdateConnectionAction
@@ -418,7 +422,7 @@ class TestUsers:
         mock_create_or_update_subscriber.assert_called_once_with(
             user=user, locale=mock_get_language_from_request.return_value
         )
-        mock_track.assert_called_once_with(user, TrackingEvent("userUpdated"))
+        self._validate_tracking_event_user_updated(mock_track)
 
     @mock.patch("openbook_auth.tasks.requests.patch")
     @mock.patch("openbook_auth.schema.mutations.publish_user_name_updated_event")
@@ -519,7 +523,89 @@ class TestUsers:
         mock_create_or_update_subscriber.assert_called_once_with(
             user=user, locale=mock_get_language_from_request.return_value
         )
-        mock_track.assert_called_once_with(user, TrackingEvent("userUpdated"))
+        self._validate_tracking_event_user_updated(mock_track)
+
+    @mock.patch("openbook_auth.tasks.requests.patch")
+    @mock.patch("openbook_auth.schema.mutations.publish_user_name_updated_event")
+    @mock.patch("openbook_auth.schema.mutations.publish_user_updated_event")
+    @mock.patch("django.utils.translation.get_language_from_request", return_value="en")
+    @mock.patch("openbook_notifications.api.NotificationApi.create_or_update_subscriber")
+    @mock.patch("openbook_auth.schema.mutations.track")
+    def test_user_updated_tracking_event(
+        self,
+        mock_track,
+        mock_create_or_update_subscriber,
+        mock_get_language_from_request,
+        mock_publish_user_name_updated_event,
+        mock_publish_user_updated_event,
+        mock_patch,
+    ):
+        response = self.create_response(200)
+        mock_patch.return_value = response
+        topic = make_topic()
+        skill = make_skill()
+        sdg = make_sdg()
+        user = make_user(name="John", last_name="Doe", engagement_level=EngagementLevelChoice.ENGAGED.value)
+
+        def update_user(inputs: Dict[str, any]) -> None:
+            request = HttpRequest()
+            request.COOKIES = {}
+            request.user = user
+            executed = async_to_sync(schema.execute)(
+                """
+                mutation updateAuthenticatedUserV2($input: UpdateAuthenticatedUserInput!) {
+                    updateAuthenticatedUserV2(input: $input) { id }
+                }
+            """,
+                context_value=benedict({"request": request}),
+                variable_values={"input": inputs},
+            )
+            assert executed.errors is None
+
+        update_user({})
+        mock_track.assert_not_called()
+        mock_track.reset_mock()
+
+        # should not contain updated_fields if field values didn't change
+        update_user({"firstName": "John", "lastName": "Doe"})
+        mock_track.assert_not_called()
+        mock_track.reset_mock()
+
+        update_user({"firstName": "Johnny", "lastName": "Doey", "aboutMe": "All about me"})
+        self._validate_tracking_event_user_updated(mock_track, {"firstName", "lastName", "aboutMe"})
+        mock_track.reset_mock()
+
+        update_user({"location": "New York", "geolocation": geolocation})
+        self._validate_tracking_event_user_updated(mock_track, {"location", "geolocation"})
+        mock_track.reset_mock()
+
+        update_user({"pronouns": "They/Them", "engagementLevel": EngagementLevelChoice.INITIATOR.value})
+        self._validate_tracking_event_user_updated(mock_track, {"pronouns", "engagementLevel"})
+        mock_track.reset_mock()
+
+        update_user({"interests": [str(topic.id)], "sdgs": [str(sdg.id)], "skills": [str(skill.id)]})
+        self._validate_tracking_event_user_updated(mock_track, {"interests", "sdgs", "skills"})
+        mock_track.reset_mock()
+
+        current_dir = os.path.dirname(os.path.abspath(__file__))
+        test_image_path = os.path.join(current_dir, "test_avatar.jpg")
+        with open(test_image_path, "rb") as image_file:
+            image_content = image_file.read()
+            image = SimpleUploadedFile(name="test_avatar.jpg", content=image_content, content_type="image/jpeg")
+        update_user({"avatar": image})
+        self._validate_tracking_event_user_updated(mock_track, {"avatar"})
+        mock_track.reset_mock()
+
+    @staticmethod
+    def _validate_tracking_event_user_updated(mock_track, expected_updated_fields=None):
+        mock_track.assert_called_once()
+        user_arg, event_arg = mock_track.call_args.args
+        assert isinstance(event_arg, TrackingEvent)
+        assert event_arg.name == "userUpdated"
+        assert isinstance(event_arg.properties, dict)
+        assert isinstance(event_arg.properties["updatedFields"], list)
+        if expected_updated_fields:
+            assert set(event_arg.properties["updatedFields"]) == expected_updated_fields
 
     @mock.patch("openbook_auth.tasks.requests.put")
     @mock.patch("openbook_auth.tasks.requests.patch")
diff --git a/openbook_auth/tests/test_user_model.py b/openbook_auth/tests/test_user_model.py
index 139aa78393b532a015cd7e26613af36c184a9184..fa2925c7c3785edc9dba7bd1e14bf26194855b05 100644
--- a/openbook_auth/tests/test_user_model.py
+++ b/openbook_auth/tests/test_user_model.py
@@ -4,7 +4,7 @@ from django.test import TransactionTestCase
 from rest_framework.exceptions import PermissionDenied, ValidationError
 
 from comments.models import Comment
-from openbook_auth.models import EngagementLevelChoice, User
+from openbook_auth.models import User
 from openbook_common.enums import VisibilityType
 from openbook_common.tests.assertions import TestAssertionsMixin
 from openbook_common.tests.helpers import (
@@ -248,15 +248,6 @@ class UserModelTest(TestAssertionsMixin, TransactionTestCase):
 
         self.assertEqual(space.posts.count(), 0)
 
-    def test_update_engagement_level(self):
-        user = make_user()
-
-        user.update_engagement_level(EngagementLevelChoice.INITIATOR)
-        self.assertEqual(user.profile.engagement_level, EngagementLevelChoice.INITIATOR)
-
-        user.update_engagement_level(EngagementLevelChoice.INTERESTED)
-        self.assertEqual(user.profile.engagement_level, EngagementLevelChoice.INTERESTED)
-
     def test_update_tracking_consent(self):
         user = make_user()
 
diff --git a/openbook_common/helpers.py b/openbook_common/helpers.py
index c5472a57bd5c7a6c01bad69b593f0b81f756eb5e..2faad837c16bfdcc84c4218452774db0713e4f10 100644
--- a/openbook_common/helpers.py
+++ b/openbook_common/helpers.py
@@ -6,21 +6,39 @@ from urllib.parse import urlparse
 import blurhash
 import requests
 import strawberry
+from PIL import ExifTags, Image
+from PIL.Image import Transpose
 from django.conf import settings
 from django.db.models import Q
 from langdetect import DetectorFactory
-from langdetect.lang_detect_exception import LangDetectException
-from PIL import ExifTags, Image
-from PIL.Image import Transpose
 from urlextract import URLExtract
 
 from openbook.settings import ALERT_HOOK_URL
-from openbook_common.utils.model_loaders import get_language_model
 
 # seed the language detector
 DetectorFactory.seed = 0
 
 extractor = URLExtract()
+stop_left = extractor.get_stop_chars_left()
+stop_left.add("\u00a0")
+stop_left.add("\u2007")
+stop_left.add("\u202f")
+stop_left.add("\u2060")
+extractor.set_stop_chars_left(stop_left)
+
+stop_right = extractor.get_stop_chars_right()
+stop_right.add("\u00a0")
+stop_right.add("\u2007")
+stop_right.add("\u202f")
+stop_right.add("\u2060")
+extractor.set_stop_chars_right(stop_right)
+
+after_tld = extractor.get_after_tld_chars()
+after_tld.append("\u00a0")
+after_tld.append("\u2007")
+after_tld.append("\u202f")
+after_tld.append("\u2060")
+extractor.set_after_tld_chars(after_tld)
 
 
 def extract_urls_from_string(text):
diff --git a/openbook_common/models.py b/openbook_common/models.py
index 0cd1c2f921ab244b03224e6ac5ed0475084b533f..c46c2406696861cb2dbd2975609b89940462bd80 100644
--- a/openbook_common/models.py
+++ b/openbook_common/models.py
@@ -1,25 +1,26 @@
 from __future__ import annotations
+
 import uuid
+from typing import List
 from urllib.parse import urlparse, urljoin, urlsplit
 
+import requests
+import tldextract
+from bs4 import BeautifulSoup
 from django.conf import settings
-from django.core.validators import RegexValidator
 from django.contrib.gis.db import models
 from django.contrib.gis.db.models import Q
+from django.core.validators import RegexValidator
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
 from ordered_model.models import OrderedModel
 from requests import RequestException
 
 from openbook.settings import COLOR_ATTR_MAX_LENGTH
-from openbook_common.helpers import extract_urls_from_string, attr_filled
 from openbook_common.enums import NeedUpdateType
+from openbook_common.helpers import extract_urls_from_string, attr_filled
 from openbook_common.utils.helpers import normalize_url
 from openbook_common.validators import hex_color_validator
-import tldextract
-import requests
-from bs4 import BeautifulSoup
-from typing import List
 
 
 class ModelWithUUID(models.Model):
@@ -172,7 +173,7 @@ class LinkPreview(ModelWithUUID):
         if link_urls:
             for link_url in link_urls:
                 preview = cls.from_url(link_url)
-                if preview.has_data():
+                if preview:
                     previews.append(preview)
         return previews
 
@@ -185,29 +186,39 @@ class LinkPreview(ModelWithUUID):
         ):
             return preview
 
-        html = LinkPreview._fetch_html_of_url(normalized_url)
-        og = cls.get_opengraph(html, url=normalized_url)
         preview.fetched_at = timezone.now()
+        html = LinkPreview._fetch_html_of_url(normalized_url)
+        if html:
+            og = cls.get_opengraph(html, url=normalized_url)
 
-        for field in cls.fields:
-            value = og.get(field[3:])
-            if value is not None:
-                setattr(preview, field[3:], value)
+            for field in cls.fields:
+                value = og.get(field[3:])
+                if value is not None:
+                    setattr(preview, field[3:], value)
 
-        preview.icon = LinkPreview._fetch_icon(html, url=normalized_url)
+            preview.icon = LinkPreview._fetch_icon(html, url=normalized_url)
+        else:
+            if not created:
+                for field in cls.fields:
+                    setattr(preview, field[3:], "")
 
         preview.save()
         return preview
 
     @staticmethod
     def _fetch_html_of_url(url: str) -> BeautifulSoup:
-        response = requests.get(url=url, timeout=10)
-        if hasattr(response, "apparent_encoding") and response.apparent_encoding:
-            response.encoding = response.apparent_encoding
-        else:
-            response.encoding = "utf-8"  # Set to utf-8 by default if apparent_encoding is not found
-
-        return BeautifulSoup(response.text, "html.parser")
+        try:
+            response = requests.get(url=url, timeout=10)
+            if not response.ok:
+                return None
+            if hasattr(response, "apparent_encoding") and response.apparent_encoding:
+                response.encoding = response.apparent_encoding
+            else:
+                response.encoding = "utf-8"  # Set to utf-8 by default if apparent_encoding is not found
+
+            return BeautifulSoup(response.text, "html.parser")
+        except requests.exceptions.RequestException:
+            return None
 
     @classmethod
     def get_opengraph(cls, html: BeautifulSoup, url: str) -> dict[str, str]:
diff --git a/openbook_common/tests/helpers.py b/openbook_common/tests/helpers.py
index 1949b684f7966d0a09f07b36b26635fa8e0c8acb..426fdef80930649534b55ae1ecaee1004c5ae2d3 100644
--- a/openbook_common/tests/helpers.py
+++ b/openbook_common/tests/helpers.py
@@ -85,6 +85,7 @@ def make_user(
     tracking_consent_personalization=False,
     role: UserRole = UserRole.REGULAR,
     is_employee: bool = False,
+    engagement_level=None,
 ) -> User:
     defined_arguments = {k: v for k, v in locals().items() if v is not None}
     defined_arguments.setdefault("username", str(uuid.uuid4()))
@@ -184,14 +185,6 @@ def make_user_avatar():
     return tmp_file
 
 
-def make_user_cover():
-    image = Image.new("RGB", (100, 100))
-    tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg")
-    image.save(tmp_file)
-    tmp_file.seek(0)
-    return tmp_file
-
-
 def make_post_image():
     image = Image.new("RGB", (100, 100))
     tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg")
@@ -272,11 +265,11 @@ def make_category():
     return mixer.blend(Category)
 
 
-def make_topic(is_discover_spaces_default=False):
+def make_topic(is_discover_spaces_default=False) -> Topic:
     return mixer.blend(Topic, is_discover_spaces_default=is_discover_spaces_default)
 
 
-def make_sdg():
+def make_sdg() -> SDG:
     return mixer.blend(SDG)
 
 
@@ -290,7 +283,7 @@ def make_community_topics():
     return community_topics
 
 
-def make_skill():
+def make_skill() -> Skill:
     return mixer.blend(Skill)
 
 
diff --git a/openbook_common/tests/test_link_preview.py b/openbook_common/tests/test_link_preview.py
index 00afb0669fe33b6d4914078b4ad70633e5e826ae..b9fda69ad11b1286e2200f19ae8cb4e7a4a1be64 100644
--- a/openbook_common/tests/test_link_preview.py
+++ b/openbook_common/tests/test_link_preview.py
@@ -1,7 +1,9 @@
 from unittest import mock
 
+from django.conf import settings
 from django.test import TestCase
 from django.utils import timezone
+from requests import RequestException
 
 from openbook_common.models import LinkPreview
 from openbook_common.utils.test import MockResponse
@@ -16,13 +18,7 @@ class LinkPreviewTestCase(TestCase):
         self.assertIn(mock.call(url="http://www.abcd.de/", timeout=10), mock_get.call_args_list)
         self.assertEqual(1, len(mock_get.call_args_list))
         self.assertEqual(preview, preview2)
-        self.assertEqual(preview.has_data(), False)
-        self.assertEqual(preview.requested_url, "http://www.abcd.de/")
-        self.assertEqual(preview.url, "")
-        self.assertEqual(preview.title, "")
-        self.assertEqual(preview.description, "")
-        self.assertEqual(preview.type, "")
-        self.assertEqual(preview.image, "")
+        self.assert_empty_preview(preview, "http://www.abcd.de/")
         self.assertTrue(timezone.now() - timezone.timedelta(seconds=3) <= preview.fetched_at <= timezone.now())
 
     @mock.patch("requests.get")
@@ -43,14 +39,17 @@ class LinkPreviewTestCase(TestCase):
         self.assertTrue(timezone.now() - timezone.timedelta(seconds=3) <= preview.fetched_at <= timezone.now())
 
     @mock.patch("requests.get")
-    def test_preview_without_data_is_note_returned(self, mock_get):
+    def test_preview_without_data_gives_empty_preview(self, mock_get):
         mock_get.return_value = MockResponse("<html>no opengraph</html>")
         previews = LinkPreview.parse_text("www.abcd.de")
         previews2 = LinkPreview.parse_text("www.abcd.de")
         self.assertIn(mock.call(url="http://www.abcd.de/", timeout=10), mock_get.call_args_list)
         self.assertEqual(1, len(mock_get.call_args_list))
         self.assertEqual(previews, previews2)
-        self.assertEqual(previews, [])
+        self.assertEqual(1, len(previews))
+        preview = previews[0]
+        self.assert_empty_preview(preview, "http://www.abcd.de/")
+        self.assertTrue(timezone.now() - timezone.timedelta(seconds=3) <= preview.fetched_at <= timezone.now())
 
     @mock.patch("requests.get")
     def test_page_with_opengraph_gives_preview(self, mock_get):
@@ -129,3 +128,79 @@ class LinkPreviewTestCase(TestCase):
         self.assertEqual(preview2.description, "MySecondDescription")
         self.assertEqual(preview2.type, "other")
         self.assertEqual(preview2.image, "https://mysecond.url/secondimage.jpg")
+
+    @mock.patch("requests.get")
+    def test_unsuccessfull_response(self, mock_get):
+        mock_get.return_value = MockResponse("", False)
+        self.parse_empty_response("www.my.de", mock_get)
+
+    @mock.patch("requests.get")
+    def test_no_response(self, mock_get):
+        mock_get.side_effect = RequestException()
+        self.parse_empty_response("www.my.de", mock_get)
+
+    def parse_empty_response(self, url, mock_get):
+        previews = LinkPreview.parse_text(url)
+        self.assertIn(mock.call(url=f"http://{url}/", timeout=10), mock_get.call_args_list)
+        self.assertEqual(1, len(previews))
+        preview = previews[0]
+        self.assert_empty_preview(preview, f"http://{url}/")
+
+    def assert_empty_preview(self, preview, url):
+        self.assertEqual(preview.has_data(), False)
+        self.assertEqual(preview.requested_url, url)
+        self.assertEqual(preview.url, "")
+        self.assertEqual(preview.title, "")
+        self.assertEqual(preview.description, "")
+        self.assertEqual(preview.type, "")
+        self.assertEqual(preview.image, "")
+
+    @mock.patch("requests.get")
+    def test_previous_preview_no_longer_exists(self, mock_get):
+        mock_get.return_value = MockResponse(
+            """
+            <html><head>
+            <meta property="og:url" content="https://my.url/xyz" />
+            <meta property="og:type" content="website" />
+            <meta property="og:title" content="MyTitle" />
+            <meta property="og:site_name" content="MySite" />
+            <meta property="og:description" content="MyDescription" />
+            <meta property="og:image" content="https://www.my.url/image.jpg" />
+            </head></html>
+            """
+        )
+        previews = LinkPreview.parse_text("www.my.de")
+        self.assertIn(mock.call(url="http://www.my.de/", timeout=10), mock_get.call_args_list)
+        self.assertEqual(1, len(previews))
+        preview = previews[0]
+        preview.fetched_at = timezone.now() - timezone.timedelta(hours=(settings.LINK_PREVIEW_CACHE_TIME_H + 1))
+        preview.save()
+
+        mock_get.return_value = MockResponse("", False)
+        self.parse_empty_response("www.my.de", mock_get)
+
+    @mock.patch("requests.get")
+    def test_non_breaking_space(self, mock_get):
+        mock_get.return_value = MockResponse(
+            """
+            <html><head>
+            <meta property="og:url" content="https://my.url/xyz" />
+            <meta property="og:type" content="website" />
+            <meta property="og:title" content="MyTitle" />
+            <meta property="og:site_name" content="MySite" />
+            <meta property="og:description" content="MyDescription" />
+            <meta property="og:image" content="https://www.my.url/image.jpg" />
+            </head></html>
+            """
+        )
+        previews1 = LinkPreview.parse_text("www.my.de/\u00a0x")
+        previews2 = LinkPreview.parse_text("www.my.de\u00a0x")
+        self.assertIn(mock.call(url="http://www.my.de/", timeout=10), mock_get.call_args_list)
+        self.assertEqual(previews1, previews2)
+        self.assertEqual(1, len(previews1))
+        preview = previews1[0]
+        self.assertEqual(preview.has_data(), True)
+        self.assertEqual(preview.requested_url, "http://www.my.de/")
+        self.assertEqual(preview.url, "https://my.url/xyz")
+        self.assertEqual(preview.title, "MyTitle")
+        preview.fetched_at = timezone.now() - timezone.timedelta(hours=(settings.LINK_PREVIEW_CACHE_TIME_H + 1))
diff --git a/openbook_insights/schema/queries.py b/openbook_insights/schema/queries.py
index f58e6af893a7a26cbd19f0dd7c6dd41111a0e2df..fda3e0fd05c8898ac812c03f2dd7c73f557917fc 100644
--- a/openbook_insights/schema/queries.py
+++ b/openbook_insights/schema/queries.py
@@ -33,22 +33,20 @@ class Query:
     @strawberry_django.field()
     def last_insights(self, info, offset: int = 0, limit: int = 10) -> Paged[Insight]:
         current_user = info.context.request.user
+
+        base_queryset = InsightModel.objects.filter(
+            Q(creator__isnull=True) | UserModel.is_visible_ignore_blocked_query("creator"),
+            date_published__lte=datetime.now(),
+        ).select_related("creator", "creator__profile", "topic")
+
+        base_queryset = base_queryset.prefetch_related("posts")
+
+        base_queryset = base_queryset.order_by("-date_published", "id")
+
         paged_insights = UserBlock.paged(
             current_user,
             lambda insight: None if insight.creator is None else insight.creator.id,
-            InsightModel.objects.filter(
-                Q(creator__isnull=True) | UserModel.is_visible_ignore_blocked_query("creator"),
-                # resolution in cache and DB is day, so result will be cached until midnight
-                date_published__lte=datetime.now(),  # timezone...
-            )
-            .select_related("creator", "creator__profile", "topic")
-            .prefetch_related(
-                "posts",
-                "posts__creator",
-                "posts__comments__commenter",
-            )
-            .order_by("-date_published", "id")
-            .cache(ops=["fetch", "count"], timeout=300),
+            base_queryset,
             offset,
             limit,
         )
diff --git a/openbook_invitations/serializers.py b/openbook_invitations/serializers.py
index 0c115a3ba5bdc93b684c92fd56cd93aa4c006f28..b2611f203bc6dce293ad9fdbc225a90908e8dc42 100644
--- a/openbook_invitations/serializers.py
+++ b/openbook_invitations/serializers.py
@@ -30,7 +30,6 @@ class InvitedUserProfileSerializer(serializers.ModelSerializer):
             "about_me",
             "url",
             "location",
-            "cover",
             "is_of_legal_age",
             "followers_count_visible",
             "badges",
diff --git a/openbook_posts/serializers/post_serializers.py b/openbook_posts/serializers/post_serializers.py
index 79e41b64d6b815e55624a8aa436e5b7f6f475f82..8bc2dc6f1dea795e211e05e1e83405d0c904ea5b 100644
--- a/openbook_posts/serializers/post_serializers.py
+++ b/openbook_posts/serializers/post_serializers.py
@@ -78,7 +78,7 @@ class PostCreatorProfileSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = UserProfile
-        fields = ("avatar", "cover", "badges", "name")
+        fields = ("avatar", "badges", "name")
 
 
 class PostImageSerializer(serializers.ModelSerializer):
@@ -265,7 +265,7 @@ class PostParticipantProfileSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = UserProfile
-        fields = ("avatar", "cover", "badges", "name")
+        fields = ("avatar", "badges", "name")
 
 
 class PostParticipantSerializer(serializers.ModelSerializer):
diff --git a/requirements.txt b/requirements.txt
index 5fb48e961eede005163a680613610894fd6ef829..100c827dd8a23265fbc0d75960dc0f0e38f43c64 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,18 +9,18 @@
 adrf~=0.1.8
 aiofiles~=24.1.0
 aiohappyeyeballs~=2.4.3
-aiohttp~=3.10.10
+aiohttp~=3.11.11
 aiosignal~=1.3.1
 ASGIMiddlewareStaticFile~=0.6.1
 asgiref~=3.8.1
 async-property~=0.2.2
-attrs~=24.2.0
+attrs~=24.3.0
 backoff~=2.2.1
-beautifulsoup4~=4.12.3
+beautifulsoup4~=4.13.0
 black~=24.10.0
 blurhash-python~=1.2.2
 cachetools~=5.5.0
-certifi~=2024.8.30
+certifi~=2024.12.14
 cffi~=1.17.1
 charset-normalizer~=3.4.0
 click~=8.1.7
@@ -42,29 +42,29 @@ django-modeltranslation~=0.19.10
 django-ordered-model~=3.7.4
 django-proxy~=1.3.0
 django-redis~=5.4.0
-django-rq~=2.10.2
+django-rq~=3.0.0
 django-sortedm2m~=4.0.0
 django-structlog~=8.1.0
 djangorestframework~=3.15.2
 djangorestframework-camel-case~=1.4.2
 execnet~=2.1.1
 Faker~=12.0.1 # mixer 7.2.2 depends on Faker<12.1 and >=5.4.0
-filelock~=3.16.1
+filelock~=3.17.0
 frozenlist~=1.5.0
 funcy~=2.0
-google-api-core~=2.22.0
-google-auth~=2.36.0
-google-cloud-pubsub~=2.26.1
-google-cloud-webrisk~=1.15.0
-googleapis-common-protos~=1.65.0
+google-api-core~=2.24.1
+google-auth~=2.38.0
+google-cloud-pubsub~=2.28.0
+google-cloud-webrisk~=1.16.0
+googleapis-common-protos~=1.66.0
 graphql-core~=3.2.5
 grpc-google-iam-v1~=0.13.1
-grpcio~=1.67.0
-grpcio-status~=1.67.1
+grpcio~=1.70.0
+grpcio-status~=1.70.0
 h11~=0.14.0
 halo~=0.0.31
-hiredis~=3.0.0
-icalendar~=6.0.1
+hiredis~=3.1.0
+icalendar~=6.1.1
 idna~=3.10
 imagekitio==2.2.8 # version 3 contains many breaking changes
 importlib_metadata~=8.4.0
@@ -84,22 +84,22 @@ opentelemetry-semantic-conventions~=0.48b0
 packaging~=24.1
 pathspec~=0.12.1
 pilkit~=3.0
-pillow~=11.0.0
+pillow~=11.1.0
 platformdirs~=4.3.6
 pluggy~=1.5.0
-posthog~=3.7.0
+posthog~=3.11.0
 propcache~=0.2.0
-proto-plus~=1.25.0
-protobuf~=5.28.3
+proto-plus~=1.26.0
+protobuf~=5.29.3
 psycopg~=3.2.3
 psycopg-binary~=3.2.3
 pyasn1~=0.6.1
 pyasn1_modules~=0.4.1
 pycparser~=2.22
-PyJWT~=2.9.0
+PyJWT~=2.10.1
 pytest~=8.3.3
 pytest-asyncio~=0.24.0
-pytest-cov~=5.0.0
+pytest-cov~=6.0.0
 pytest-django~=4.9.0
 pytest-xdist~=3.6.1
 python-benedict~=0.34.0
@@ -113,15 +113,15 @@ pytz~=2024.2
 redis~=5.2.0
 requests~=2.32.3
 requests-file~=2.1.0
-requests-toolbelt~=0.10.1
+requests-toolbelt~=1.0.0
 rest-framework-generic-relations~=2.2.0
-rq~=1.16.2
+rq~=2.1.0
 rsa~=4.9
 ruamel.yaml~=0.18.6
 ruamel.yaml.clib~=0.2.12
 ruff~=0.7.1
-sentry-sdk~=2.17.0
-six~=1.16.0
+sentry-sdk~=2.20.0
+six~=1.17.0
 soupsieve~=2.6
 spinners~=0.0.24
 sqlparse~=0.5.1
@@ -137,7 +137,7 @@ Unidecode~=1.3.8
 uritools~=4.0.3
 url-normalize~=1.4.3
 urlextract~=1.9.0
-urllib3~=1.26.20
+urllib3~=2.3.0
 uvicorn~=0.32.0
 wrapt~=1.16.0
 yarl~=1.17.1
diff --git a/templates/.docker-compose.env b/templates/.docker-compose.env
index 305a79f69484ed8c85a2145032cd2d6ac1a4dcf3..1b25c4fc57013bc7ce9f2bb07598455ade3731b5 100644
--- a/templates/.docker-compose.env
+++ b/templates/.docker-compose.env
@@ -91,7 +91,6 @@ REDIS_PASSWORD={{REDIS_PASSWORD}}
 # [OPTIONAL]
 # POST_MEDIA_MAX_SIZE=30485760
 # PROFILE_AVATAR_MAX_SIZE=10485760
-# PROFILE_COVER_MAX_SIZE=10485760
 # COMMUNITY_AVATAR_MAX_SIZE=10485760
 # COMMUNITY_COVER_MAX_SIZE=10485760