diff --git a/package-lock.json b/package-lock.json
index 3d3ff0e6b28ed1c2df2d0170423c556ff1392550..db6b3447ef3420d528ca3246732c68f2713105ed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14084,6 +14084,11 @@
         "xml-name-validator": "^3.0.0"
       }
     },
+    "wait-queue": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/wait-queue/-/wait-queue-1.1.4.tgz",
+      "integrity": "sha512-/VdMghiBDG/Ch43ZRp3d8OSd8A0dx8hfkBO7AfWCDzMn2blHquMf+3gqHHhYcggSBpKf7VTzA939bb0DevYKBA=="
+    },
     "walker": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz",
diff --git a/package.json b/package.json
index d820587936f0d023c71ef98648dda62ad7401491..3b0975013e5815096adb5a70b33f59519b168ef4 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,8 @@
     "react-modal": "^3.12.1",
     "react-router-dom": "^5.2.0",
     "react-scripts": "3.4.3",
-    "unfetch": "^4.2.0"
+    "unfetch": "^4.2.0",
+    "wait-queue": "^1.1.4"
   },
   "scripts": {
     "start": "react-scripts start",
diff --git a/src/App.jsx b/src/App.jsx
index d2ba396f315a7a5960c105fbb5383a59a7a45a11..88a4ccedf659dc3412738f83e78e2f940dd7c928 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -3,9 +3,7 @@ import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
 import { KeycloakProvider } from "@react-keycloak/web";
 import * as Sentry from "@sentry/react";
 
-import { loadGroupMappings } from "actions/misc";
-import { loadProgram } from "actions/program";
-import { loadPosts } from "actions/posts";
+import { initializeWSChannel } from "actions/ws";
 import Footer from "components/Footer";
 import Navbar from "components/Navbar";
 import Home from "pages/Home";
@@ -57,9 +55,7 @@ const LoadingComponent = (
 );
 
 const BaseApp = () => {
-  loadGroupMappings.read();
-  loadProgram.read();
-  loadPosts.read();
+  initializeWSChannel.read();
 
   return (
     <Router>
diff --git a/src/actions/misc.js b/src/actions/misc.js
index 92ea09cde20da32cb897392cb5a1456be3707c35..ca57989d4093a06a4262e7fc3d3ca06c1231e9c1 100644
--- a/src/actions/misc.js
+++ b/src/actions/misc.js
@@ -23,3 +23,4 @@ export const loadGroupMappings = createAsyncAction(
     },
   }
 );
+
diff --git a/src/actions/ws.js b/src/actions/ws.js
new file mode 100644
index 0000000000000000000000000000000000000000..59b5c6e7f0cbf60a3ff1743cff82e314e31dfd4a
--- /dev/null
+++ b/src/actions/ws.js
@@ -0,0 +1,30 @@
+import { createAsyncAction, errorResult, successResult } from "pullstate";
+
+import { connect } from "ws/connection";
+
+import { loadPosts } from "./posts";
+import { loadProgram } from "./program";
+
+export const initializeWSChannel = createAsyncAction(async () => {
+  try {
+    const wsChannel = await connect({
+      onConnect: async ({ worker }) => {
+        // Re-load initial data once connected, this will ensure we won't lose
+        // any intermediate state.
+        await Promise.all([
+          loadProgram.run({}, { respectCache: false }),
+          loadPosts.run({}, { respectCache: false }),
+        ]);
+
+        // Once loaded, start processing the messages.
+        worker.start();
+
+        return true;
+      },
+    });
+
+    return successResult(wsChannel);
+  } catch (err) {
+    return errorResult([], err.toString());
+  }
+});
diff --git a/src/index.js b/src/index.js
index 60f1b437bafe2c08bdb6d4b364c578e77ca7d05e..63f13377424e58ef3ba12bee37c012fac2069cb8 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,23 +2,19 @@ import React from "react";
 import ReactDOM from "react-dom";
 import ReactModal from "react-modal";
 
-import { connect } from "./ws/connection";
 import App from "./App";
 import * as serviceWorker from "./serviceWorker";
 
 const root = document.getElementById("root");
 
-const render = () => {
-  ReactDOM.render(
-    <React.StrictMode>
-      <App />
-    </React.StrictMode>,
-    root
-  );
-  ReactModal.setAppElement(root);
-};
+ReactDOM.render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>,
+  root
+);
 
-connect().then(render);
+ReactModal.setAppElement(root);
 
 // If you want your app to work offline and load faster, you can change
 // unregister() to register() below. Note this comes with some pitfalls.
diff --git a/src/ws/connection.js b/src/ws/connection.js
index 5d4ae399a95965221b893abfc5b52b7ba8e1eb72..ab2a5f1d863799b3e79663e0b49e4f2d111157d7 100644
--- a/src/ws/connection.js
+++ b/src/ws/connection.js
@@ -1,48 +1,77 @@
+import WaitQueue from "wait-queue";
+
 import { handleRanking } from "./handlers";
 
 const handlerMap = {
   ranked: handleRanking,
 };
 
-const messageRouter = (event) => {
-  console.debug("[ws] New message", event.data);
+function Worker() {
+  const queue = new WaitQueue();
 
-  try {
-    const data = JSON.parse(event.data);
+  const doLoop = async () => {
+    const event = await queue.shift();
+    messageRouter(event);
+    setTimeout(doLoop);
+  };
 
-    if (!data.event) {
-      return console.error("[ws] Missing `event` field");
-    }
+  const messageRouter = (event) => {
+    console.debug("[ws][worker] New message", event.data);
+
+    try {
+      const data = JSON.parse(event.data);
+
+      if (!data.event) {
+        return console.error("[ws][worker] Missing `event` field");
+      }
+
+      const handlerFn = handlerMap[data.event];
 
-    const handlerFn = handlerMap[data.event];
+      if (!handlerFn) {
+        return console.warn(`[ws][worker] Can't handle event '${data.event}'`);
+      }
 
-    if (!handlerFn) {
-      console.warn(`[ws] Can't handle event '${data.event}'`);
+      handlerFn(data.payload || {});
+    } catch (err) {
+      console.error("[ws][worker] Could not parse message as JSON.");
     }
+  };
 
-    handlerFn(data.payload);
-  } catch (err) {
-    console.error("[ws] Could not parse message as JSON.");
-  }
-};
+  return {
+    queue,
+    start: () => {
+      console.debug("[ws][worker] Start processing messages.");
+      doLoop();
+    },
+  };
+}
 
-export const connect = () => {
+export const connect = ({ onConnect }) => {
   return new Promise((resolve, reject) => {
+    const worker = Worker();
+
     const ws = new WebSocket(process.env.REACT_APP_WS_BASE_URL);
-    let keepAlive;
+    let keepAliveInterval;
     console.log("[ws] Connecting ...");
 
     ws.onopen = () => {
       console.log("[ws] Connected.");
-      resolve(ws);
 
-      keepAlive = setInterval(() => {
+      keepAliveInterval = setInterval(() => {
         ws.send("KEEPALIVE");
         console.debug("[ws] Sending keepalive.");
       }, 30 * 1000);
+
+      const self = { ws, worker };
+
+      if (onConnect) {
+        return onConnect(self).then(() => resolve(self));
+      }
+
+      return resolve(self);
     };
 
-    ws.onmessage = messageRouter;
+    ws.onmessage = worker.queue.push.bind(worker.queue);
 
     ws.onclose = (event) => {
       console.log(
@@ -50,7 +79,7 @@ export const connect = () => {
         event.reason
       );
 
-      clearInterval(keepAlive);
+      clearInterval(keepAliveInterval);
       setTimeout(connect, 1000);
     };