diff --git a/aleksis/core/assets/App.vue b/aleksis/core/assets/App.vue
index 026e5c4186ab96d9258dc1a3de98cc0e53fa2e1f..19c3ad0b9d110008ee9ce1d60b591b633c504a5f 100644
--- a/aleksis/core/assets/App.vue
+++ b/aleksis/core/assets/App.vue
@@ -185,8 +185,13 @@
       </v-app-bar>
       <v-main>
         <v-container>
-          <broadcast-channel-notification channel-name="cache-or-not" />
-          <broadcast-channel-notification channel-name="offline-fallback" />
+          <broadcast-channel-notification channel-name="cache-or-not"/>
+
+          <message-box
+              type="warning"
+              v-if="$root.offline"
+          >{{ $t("network_errors.offline_notification") }}
+          </message-box>
 
           <message-box
             type="error"
@@ -361,6 +366,7 @@ export default {
       sideNavMenu: null,
       accountMenu: null,
       snackbarItems: null,
+      ping: null,
     };
   },
   methods: {
@@ -500,6 +506,24 @@ export default {
       },
       deep: true,
     },
+    ping: function (ping) {
+      if (ping === "pong") {
+        this.$apollo.queries.ping.stopPolling();
+        this.$apollo.queries.whoAmI.startPolling(10000);
+        this.$apollo.queries.snackbarItems.startPolling(1000);
+        this.$apollo.queries.messages.startPolling(1000);
+        this.$root.offline = false;
+      }
+    },
+    "$root.offline": function (offline) {
+      if (offline) {
+        this.ping = null;
+        this.$apollo.queries.ping.startPolling(1000);
+        this.$apollo.queries.whoAmI.stopPolling();
+        this.$apollo.queries.snackbarItems.stopPolling();
+        this.$apollo.queries.messages.stopPolling();
+      }
+    },
   },
   mounted() {
     this.$router.onReady(this.getPermissionNames);
diff --git a/aleksis/core/assets/app.js b/aleksis/core/assets/app.js
index 01c0b672dcd41a9c539c4f1894e6bbf08cc8c156..14d3696bf4f9e8f24b146f44e76627e7c96654fa 100644
--- a/aleksis/core/assets/app.js
+++ b/aleksis/core/assets/app.js
@@ -128,7 +128,7 @@ const errorLink = onError(({ graphQLErrors, networkError }) => {
   if (graphQLErrors) addErrorSnackbarItem("graphql.snackbar_error_message");
 
   if (networkError)
-    addErrorSnackbarItem("network_errors.snackbar_error_message");
+    app.offline = true;
 });
 
 const httpLink = new HttpLink({
@@ -193,6 +193,7 @@ const app = new Vue({
   data: () => ({
     showCacheAlert: false,
     contentLoading: false,
+    offline: false,
   }),
   router,
   i18n,
diff --git a/aleksis/core/assets/messages/en.json b/aleksis/core/assets/messages/en.json
index b2679b64a49563ff4a9491cfe891ef4d6ecfe6f5..78cc0ab994ce48efd3944b9148be36cf3dd8b897 100644
--- a/aleksis/core/assets/messages/en.json
+++ b/aleksis/core/assets/messages/en.json
@@ -181,7 +181,8 @@
     "error_404": "404",
     "page_not_found": "The requested page or resource could not be found.",
     "take_me_back": "Take me back",
-    "snackbar_error_message": "A network error occurred. Please try again."
+    "snackbar_error_message": "A network error occurred. Please try again.",
+    "offline_notification": "You are offline. Some features may not work and some data may not be up to date."
   },
   "service_worker": {
     "new_version_available": "A new version of the app is available",
diff --git a/aleksis/core/assets/ping.graphql b/aleksis/core/assets/ping.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..1a13edeb72aa88284dd3a0ea64f8c3f4d5732c08
--- /dev/null
+++ b/aleksis/core/assets/ping.graphql
@@ -0,0 +1,3 @@
+{
+  ping
+}