diff --git a/include/ftl/protocol/self.hpp b/include/ftl/protocol/self.hpp
index ad3ba198f5f4b41d8206314c61527a4171c6bf0a..b857418c3d7e9655b4b479efaba5786d92926e39 100644
--- a/include/ftl/protocol/self.hpp
+++ b/include/ftl/protocol/self.hpp
@@ -188,6 +188,11 @@ class Self {
     // Used for testing
     ftl::net::Universe *getUniverse() const { return universe_.get(); }
 
+    // === Statistics methods ===
+
+    float getKBitsPerSecondTX() const;
+    float getKBitsPerSecondRX() const;
+
     // === The RPC methods ===
 
     /**
diff --git a/src/peer.cpp b/src/peer.cpp
index cc955545b8610b1b16f6f18186305c48b5601447..5dca8e14638e22f077dd6f1c18a65d7df2fecc14 100644
--- a/src/peer.cpp
+++ b/src/peer.cpp
@@ -338,6 +338,8 @@ void Peer::data() {
         return;
     }
 
+    net_->rxBytes_ += rc;
+
     // May possibly need locking
     recv_buf_.buffer_consumed(rc);
 
@@ -576,6 +578,7 @@ int Peer::_send() {
         _close(reconnect_on_socket_error_);
     }
 
+    net_->txBytes_ += c;
     return c;
 }
 
diff --git a/src/self.cpp b/src/self.cpp
index 7331266f47551fef4cf639324cda99b95799879d..b0bcc6392aede0dd8739dcac28a67ca4049e4707 100644
--- a/src/self.cpp
+++ b/src/self.cpp
@@ -81,6 +81,14 @@ bool Self::isConnected(const std::string &s) {
     return universe_->isConnected(s);
 }
 
+float Self::getKBitsPerSecondTX() const {
+    return universe_->getKBitsPerSecondTX();
+}
+
+float Self::getKBitsPerSecondRX() const {
+    return universe_->getKBitsPerSecondRX();
+}
+
 size_t Self::numberOfNodes() const {
     return universe_->numberOfPeers();
 }
diff --git a/src/universe.cpp b/src/universe.cpp
index 1b98f7bafb2cd209db20473cbe163b6699264508..50591f1e2fb04da7830ff797acdf784b1b6f1fd1 100644
--- a/src/universe.cpp
+++ b/src/universe.cpp
@@ -16,6 +16,8 @@
 #define LOGURU_REPLACE_GLOG 1
 #include <ftl/lib/loguru.hpp>
 
+#include <ftl/time.hpp>
+
 #include <nlohmann/json.hpp>
 
 #include "protocol/connection.hpp"
@@ -425,6 +427,15 @@ std::list<PeerPtr> Universe::getPeers() const {
 }
 
 void Universe::_periodic() {
+    // Update stats
+    int64_t now = ftl::time::get_time();
+    float seconds = static_cast<float>(now - stats_lastTS_) / 1000.0f;
+    stats_rxkbps_ = static_cast<float>(rxBytes_) / seconds / 1024.0f;
+    rxBytes_ = 0;
+    stats_txkbps_ = static_cast<float>(txBytes_) / seconds / 1024.0f;
+    txBytes_ = 0;
+    stats_lastTS_ = now;
+
     auto i = reconnects_.begin();
     while (i != reconnects_.end()) {
         std::string addr = i->peer->getURI();
diff --git a/src/universe.hpp b/src/universe.hpp
index 1b0cc090bf6338d55add0cafd88f2282b447ca19..3e5ebeccdb5f3bd2f0a2b408555293eae6bfbeec 100644
--- a/src/universe.hpp
+++ b/src/universe.hpp
@@ -170,6 +170,9 @@ class Universe {
     void setSendBufferSize(ftl::URI::scheme_t s, size_t size);
     void setRecvBufferSize(ftl::URI::scheme_t s, size_t size);
 
+    float getKBitsPerSecondTX() const { return stats_txkbps_ * 8.0f; }
+    float getKBitsPerSecondRX() const { return stats_rxkbps_ * 8.0f; }
+
     static inline std::shared_ptr<Universe> getInstance() { return instance_; }
 
     void setMaxConnections(size_t m);
@@ -203,6 +206,13 @@ class Universe {
 
     std::unique_ptr<NetImplDetail> impl_;
 
+    // Statistics data.
+    float stats_txkbps_ = 0.0f;
+    float stats_rxkbps_ = 0.0f;
+    std::atomic_size_t txBytes_ = 0;
+    std::atomic_size_t rxBytes_ = 0;
+    int64_t stats_lastTS_ = 0;
+
     std::vector<std::unique_ptr<ftl::net::internal::SocketServer>> listeners_;
     std::vector<ftl::net::PeerPtr> peers_;
     std::unordered_map<std::string, size_t> peer_by_uri_;