diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ba27f6da08d3d2bbc3dc5a5add6d432e09b9cf13..cc612eb9459b81e5be3501e20bafd92ea23d3dc3 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -267,3 +267,8 @@ test-set-affinity-on-block:
   extends:
     - .test
     - .set-affinity-on-block
+
+test-mmapped-log:
+  extends:
+    - .meson-test
+  script: make && EMPER_LOG_FILE=emper.log make test
diff --git a/emper/Debug.hpp b/emper/Debug.hpp
index 507399fc96c6fa6441330b1eb4042123e08091cd..6520cf6c7fd322ab8f2683e19b85496f626b3701 100644
--- a/emper/Debug.hpp
+++ b/emper/Debug.hpp
@@ -2,11 +2,14 @@
 // Copyright © 2020-2021 Florian Schmaus, Florian Fischer
 #pragma once
 
+#include <atomic>
+#include <cstddef>
 #include <sstream>	// IWYU pragma: keep
 #include <string>
 #include <string_view>
 
 #include "emper-config.h"
+#include "log/log.hpp"
 
 // If we apply clang-format to the following region, then clang-format
 // will create multi-line macros. However clang-tidy's NOLINTNEXTLINE
@@ -24,7 +27,7 @@
 #define LOG(level, x, log_func) do { if constexpr (level <= EMPER_LOG_LEVEL) { log_func(EMPER_BUILD_STR(x)); } } while (false)
 
 // NOLINTNEXTLINE(bugprone-macro-parentheses)
-#define DBG(x) LOG(Debug, x, emper_log_no_prefix);
+#define DBG(x) LOG(Debug, x, emper::log::log_no_prefix);
 
 // To avoid "error: there are no arguments to ‘logD’ that depend on a
 // template parameter, so a declaration of ‘logD’ must be available"
@@ -35,19 +38,19 @@
 #define LOGDD(x) LOG(FineDebug, x, this->logDD);
 
 // NOLINTNEXTLINE(bugprone-macro-parentheses)
-#define LOGI(x) LOG(Info, "Info: " << x, emper_log_no_prefix);
+#define LOGI(x) LOG(Info, "Info: " << x, emper::log::log_no_prefix);
 // NOLINTNEXTLINE(bugprone-macro-parentheses)
 #define LOGGER_LOGI(x) LOG(Info, "Info: " << x, this->logI);
 
 // NOLINTNEXTLINE(bugprone-macro-parentheses)
-#define LOGW(x) LOG(Warning, "Warning: " << x, emper_log_no_prefix);
+#define LOGW(x) LOG(Warning, "Warning: " << x, emper::log::log_no_prefix);
 // NOLINTNEXTLINE(bugprone-macro-parentheses)
 #define LOGGER_LOGW(x) LOG(Warning, "Warning: " << x, this->logW);
 
 // NOLINTNEXTLINE(bugprone-macro-parentheses)
-#define LOGE(x) LOG(Error, "Error: " << x, emper_log_no_prefix);
+#define LOGE(x) LOG(Error, "Error: " << x, emper::log::log_no_prefix);
 // NOLINTNEXTLINE(bugprone-macro-parentheses)
-#define LOGGER_LOGE(x) LOG(Error, "Error: " << x, emper_log_no_prefix);
+#define LOGGER_LOGE(x) LOG(Error, "Error: " << x, emper::log::log_no_prefix);
 
 // NOLINTNEXTLINE(bugprone-macro-parentheses)
 #define ABORT(x) { LOGE(x); abort(); }
@@ -92,12 +95,6 @@ enum LogLevel {
 	ALL,
 };
 
-void emper_add_timestamp_to(std::ostringstream& logMessage);
-
-void emper_log(const std::string& prefix, const std::string& message);
-
-static inline void emper_log_no_prefix(const std::string& message) { emper_log("", message); }
-
 template <LogSubsystem logSubsystem>
 class Logger {
  private:
@@ -171,7 +168,7 @@ class Logger {
 			sst << " " << this;
 		}
 
-		emper_log(sst.str(), string);
+		emper::log::log(sst.str(), string);
 	}
 
 	inline void logE(const std::string& string) const { log<Error>(string); }
diff --git a/emper/Runtime.cpp b/emper/Runtime.cpp
index 0ca865bd2ddf2f4ce463cda408feb25876a4747f..4ea9687c5664d500523f86ea0f7152e0976d284a 100644
--- a/emper/Runtime.cpp
+++ b/emper/Runtime.cpp
@@ -33,6 +33,7 @@
 #include "io/IoContext.hpp"
 #include "io/Stats.hpp"
 #include "lib/DebugUtil.hpp"
+#include "log/LogBuffer.hpp"
 #include "stats/FromAnywhere.hpp"
 #include "stats/Worker.hpp"
 #include "strategies/AbstractWorkStealingScheduler.hpp"
@@ -95,6 +96,11 @@ Runtime::Runtime(workerid_t workerCount, RuntimeStrategyFactory& strategyFactory
 		currentRuntime = this;
 	}
 
+	const char* envValueLogFile = std::getenv("EMPER_LOG_FILE");
+	if (envValueLogFile) {
+		logBuffer = new emper::log::LogBuffer(envValueLogFile);
+	}
+
 	if constexpr (emper::STATS) {
 		workerStats = new WorkerLocalData<emper::stats::Worker>(
 				[workerCount] {
@@ -227,12 +233,15 @@ Runtime::~Runtime() {
 		delete fromAnywhereStats;
 	}
 
+	DBG("Runtime " << this << " terminated");
+
 	// TODO: Move this before the delete invocations?
 	{
 		std::lock_guard<std::mutex> lock(currentRuntimeMutex);
 		currentRuntime = nullptr;
 	}
-	DBG("Runtime " << this << " terminated");
+
+	delete logBuffer;
 }
 
 auto Runtime::workerLoop(Worker* worker) -> void* {
diff --git a/emper/Runtime.hpp b/emper/Runtime.hpp
index efe1cbe8178a330ae79d74ab3cfad7ca8ffd1c2e..c9f71733893a3cc77818da15daa23b8cd38908dd 100644
--- a/emper/Runtime.hpp
+++ b/emper/Runtime.hpp
@@ -44,6 +44,9 @@ namespace io {
 class GlobalIoContext;
 class IoContext;
 }	 // namespace io
+namespace log {
+class LogBuffer;
+}
 namespace stats {
 class Worker;
 class FromAnywhere;
@@ -89,6 +92,8 @@ class Runtime : public Logger<LogSubsystem::RUNTI> {
 	WorkerLocalData<emper::stats::Worker>* workerStats;
 	emper::stats::FromAnywhere* fromAnywhereStats;
 
+	emper::log::LogBuffer* logBuffer = nullptr;
+
 	auto workerLoop(Worker* worker) -> void*;
 
 	WorkerSleepStrategy workerSleepStrategy;
@@ -210,5 +215,5 @@ class Runtime : public Logger<LogSubsystem::RUNTI> {
 	template <typename>
 	friend class WorkerLocalData;
 
-	friend void emper_log(const std::string& prefix, const std::string& message);
+	friend void emper::log::log(const std::string& prefix, const std::string& message);
 };
diff --git a/emper/io/GlobalIoContext.hpp b/emper/io/GlobalIoContext.hpp
index 862c944afba565523c84cd42af41f9d1b29ff99d..611793f2a34e9f1861420a346d4d090d59c50e8f 100644
--- a/emper/io/GlobalIoContext.hpp
+++ b/emper/io/GlobalIoContext.hpp
@@ -15,7 +15,7 @@ class Runtime;
 namespace emper::io {
 class GlobalIoContext : public IoContext {
 	friend Runtime;
-	friend void ::emper_log(const std::string& prefix, const std::string& message);
+	friend void emper::log::log(const std::string& prefix, const std::string& message);
 	friend class Future;
 	friend class SendFuture;
 	friend class RecvFuture;
diff --git a/emper/log/LogBuffer.cpp b/emper/log/LogBuffer.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..790abb9b51208d22621bbac232bb9c01e45e8a6d
--- /dev/null
+++ b/emper/log/LogBuffer.cpp
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright © 2020 Florian Schmaus
+#include "LogBuffer.hpp"
+
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <cstring>
+#include <ostream>
+
+#include "Common.hpp"
+
+namespace emper::log {
+
+LogBuffer::LogBuffer(const std::string& logFile) : logFile(logFile) {
+	logFd = open(logFile.c_str(), O_RDWR | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
+	if (logFd == -1) {
+		DIE_MSG_ERRNO("opening log file " << logFile << " failed");
+	}
+
+	for (size_t i = 0; i < BUFFER_COUNT; ++i) {
+		const auto off = static_cast<off_t>(i * BUFFER_SIZE);
+		void* res = mmap(nullptr, BUFFER_SIZE, PROT_WRITE, MAP_FILE | MAP_SHARED, logFd, off);
+		// NOLINTNEXTLINE(performance-no-int-to-ptr)
+		if (res == MAP_FAILED) {
+			DIE_MSG_ERRNO("mmap failed");
+		}
+		bufs[i] = static_cast<char*>(res);
+	}
+
+	if (ftruncate(logFd, BUFFER_COUNT * BUFFER_SIZE)) {
+		DIE_MSG_ERRNO("initial log file " << logFile << "truncation failed");
+	}
+}
+
+LogBuffer::~LogBuffer() {
+	auto finalPos = static_cast<off_t>(bufPos.load(std::memory_order_relaxed));
+	if (ftruncate(logFd, finalPos)) {
+		DIE_MSG_ERRNO("trimming log file " << logFile << " failed");
+	}
+
+	for (auto* buf : bufs) {
+		if (munmap(buf, BUFFER_SIZE)) {
+			DIE_MSG_ERRNO("munmap failed");
+		}
+	}
+
+	close(logFd);
+}
+
+void LogBuffer::log(const std::string& message) {
+	const size_t messageLen = message.size();
+	const size_t startPos = bufPos.fetch_add(messageLen, std::memory_order_relaxed);
+	const size_t endPos = startPos + messageLen - 1;
+	char* buf = getBuf(startPos);
+	const size_t bufOffset = startPos % BUFFER_SIZE;
+	char* msgEndBuf = getBuf(endPos);
+
+	// The message fits completly into the current buffer
+	if (buf == msgEndBuf) {
+		// NOLINTNEXTLINE(bugprone-not-null-terminated-result)
+		memcpy(&buf[bufOffset], message.c_str(), message.size());
+
+		// Are we still in the active buffer?
+		if (bufOffset > 0) {
+			return;
+		}
+		// The message spans the active and the next buffer
+	} else {
+		const size_t leftInActive = BUFFER_SIZE - (startPos % BUFFER_SIZE);
+		// NOLINTNEXTLINE(bugprone-not-null-terminated-result)
+		memcpy(&buf[bufOffset], message.c_str(), leftInActive);
+		// NOLINTNEXTLINE(bugprone-not-null-terminated-result)
+		memcpy(&msgEndBuf[0], &message.c_str()[leftInActive], message.size() - leftInActive);
+	}
+
+	// This is the first overflow there is no old buffer to remap
+	if (bufPos < BUFFER_SIZE * 2) {
+		return;
+	}
+
+	// We are the first writing to the next buffer therefore we are responsible
+	// to remap the old.
+
+	// NOTE: This is NOT sound it is possible that other threads are still using the old
+	// buffer. But with big enough BUFFER_SIZE it is likely that all threads are using
+	// the current active buffer
+
+	// Active buffer -> old buffer
+	// Fresh buffer -> active buffer
+	// Remap old buffer -> next fresh buffer
+
+	// Remap the old buffer
+	const size_t oldBufIdx = ((startPos / BUFFER_SIZE) - 1) % BUFFER_COUNT;
+	char* oldBufPtr = bufs[oldBufIdx];
+
+	// The buffer [0, BUFFER_SIZE) is the first active buffer
+	// Therefore at bufPos = BUFFER_SIZE + 1 we are in the second active buffer
+	const size_t nthActive = (endPos / BUFFER_SIZE) + 1;
+
+	// Our log file has the size of all ever active buffers plus the new fresh one
+	const auto logFileSize = static_cast<off_t>((nthActive + 1) * BUFFER_SIZE);
+
+	// Grow the log file
+	if (ftruncate(logFd, logFileSize)) {
+		DIE_MSG_ERRNO("growing log file " << logFile << " failed");
+	}
+
+	const auto nextOffset = static_cast<off_t>(nthActive * BUFFER_SIZE);
+	void* res = mmap(nullptr, BUFFER_SIZE, PROT_WRITE, MAP_FILE | MAP_SHARED, logFd, nextOffset);
+	// NOLINTNEXTLINE(performance-no-int-to-ptr)
+	if (res == MAP_FAILED) {
+		DIE_MSG_ERRNO("mmap of fresh buffer failed");
+	}
+	bufs[oldBufIdx] = static_cast<char*>(res);
+
+	if (munmap(oldBufPtr, BUFFER_SIZE)) {
+		DIE_MSG_ERRNO("munmap of replaced buffer failed");
+	}
+}
+
+}	 // namespace emper::log
diff --git a/emper/log/LogBuffer.hpp b/emper/log/LogBuffer.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8518b2c8fd6b091e25edb50a36e508fee62abb0f
--- /dev/null
+++ b/emper/log/LogBuffer.hpp
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright © 2021 Florian Fischer
+#pragma once
+
+#include <atomic>
+#include <cstddef>
+#include <string>
+
+namespace emper::log {
+class LogBuffer {
+	// We use a triple buffer scheme, where one buffer is active, one is old and one is fresh
+	// The old buffer gets remapped by the first accessing the fresh buffer
+	static const int BUFFER_COUNT = 3;
+	static const size_t BUFFER_SIZE = 1 << 20;
+
+	char* bufs[BUFFER_COUNT];
+
+	std::atomic<size_t> bufPos = 0;
+
+	std::string logFile;
+	int logFd;
+
+	auto getBuf(size_t pos) -> char* { return bufs[(pos / BUFFER_SIZE) % BUFFER_COUNT]; }
+
+ public:
+	LogBuffer(const std::string& logFile);
+	~LogBuffer();
+
+	void log(const std::string& message);
+};
+}	 // namespace emper::log
diff --git a/emper/Debug.cpp b/emper/log/log.cpp
similarity index 76%
rename from emper/Debug.cpp
rename to emper/log/log.cpp
index 953fef6ffe30d92cc77752cf203c0d953803b288..97898272f18f4bfcadf8ec7ababcf5f104d85d1f 100644
--- a/emper/Debug.cpp
+++ b/emper/log/log.cpp
@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
 // Copyright © 2020 Florian Schmaus
-#include "Debug.hpp"
+// Copyright © 2021 Florian Fischer
+#include "log.hpp"
 
 #include <pthread.h>
 
@@ -15,14 +16,14 @@
 #include "Runtime.hpp"
 #include "Worker.hpp"
 #include "emper-common.h"	 // for workerid_t
-#include "emper-config.h"	 // for EMPER_LOG_LEVEL
 #include "io/GlobalIoContext.hpp"
+#include "log/LogBuffer.hpp"
 
 using emper::io::GlobalIoContext;
 
-static std::mutex emper_log_mutex;
+namespace emper::log {
 
-void emper_add_timestamp_to(std::ostringstream& logMessage) {
+static void add_timestamp_to(std::ostringstream& logMessage) {
 	auto now = std::chrono::high_resolution_clock::now();
 
 	auto now_time_t = std::chrono::high_resolution_clock::to_time_t(now);
@@ -37,7 +38,10 @@ void emper_add_timestamp_to(std::ostringstream& logMessage) {
 	logMessage << remaining_nanos;
 }
 
-void emper_log(const std::string& prefix, const std::string& message) {
+static std::mutex log_mutex;
+
+void log(const std::string& prefix, const std::string& message) {
+	Runtime* runtime = Runtime::getRuntime();
 	Worker* worker = Worker::getCurrentWorker();
 	std::ostringstream logMessage;
 	if (likely(worker)) {
@@ -46,7 +50,6 @@ void emper_log(const std::string& prefix, const std::string& message) {
 		logMessage << std::setfill('0') << std::setw(3) << workerIdAsString << " ";
 	} else {
 		// Are we the global IO completer
-		Runtime* runtime = Runtime::getRuntime();
 		GlobalIoContext* gio = runtime ? runtime->globalIo : nullptr;
 		if (gio && pthread_self() == gio->completer) {
 			logMessage << "IOC ";
@@ -56,7 +59,7 @@ void emper_log(const std::string& prefix, const std::string& message) {
 	}
 
 	if constexpr (emper::LOG_TIMESTAMP) {
-		emper_add_timestamp_to(logMessage);
+		add_timestamp_to(logMessage);
 	}
 
 	if (!prefix.empty()) {
@@ -67,11 +70,11 @@ void emper_log(const std::string& prefix, const std::string& message) {
 
 	logMessage << message << std::endl;
 
-	std::unique_lock<std::mutex> lock(emper_log_mutex);
-	std::cerr << logMessage.str();
-}
-
-// global log_level which can be changed to control the log output
-namespace emper {
-enum LogLevel log_level = EMPER_LOG_LEVEL;
+	if (likely(runtime) && runtime->logBuffer) {
+		runtime->logBuffer->log(logMessage.str());
+	} else {
+		std::unique_lock<std::mutex> lock(log_mutex);
+		std::cerr << logMessage.str();
+	}
 }
+}	 // namespace emper::log
diff --git a/emper/log/log.hpp b/emper/log/log.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9bab9ca259b09ef1f5e50073efb7b622740a6151
--- /dev/null
+++ b/emper/log/log.hpp
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright © 2020-2021 Florian Schmaus, Florian Fischer
+#pragma once
+
+#include <string>
+
+namespace emper::log {
+void log(const std::string& prefix, const std::string& message);
+
+static inline void log_no_prefix(const std::string& message) { log("", message); }
+}	 // namespace emper::log
diff --git a/emper/log/meson.build b/emper/log/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..6de1e134d89b7135ad80df2700b20a05a368eac7
--- /dev/null
+++ b/emper/log/meson.build
@@ -0,0 +1,4 @@
+emper_cpp_sources += files(
+  'log.cpp',
+  'LogBuffer.cpp',
+)
diff --git a/emper/meson.build b/emper/meson.build
index 5e827d84a94854a66dc5a45437c101080a7c237d..e5b942b972dc96bf175fd3ad34e7d5e9a2c151e7 100644
--- a/emper/meson.build
+++ b/emper/meson.build
@@ -21,7 +21,6 @@ emper_cpp_sources = [
   'Scheduler.cpp',
   'Dispatcher.cpp',
   'Common.cpp',
-  'Debug.cpp',
   'ContextManager.cpp',
   'BinaryPrivateSemaphore.cpp',
   'CountingPrivateSemaphore.cpp',
@@ -39,6 +38,7 @@ emper_library_include += include_directories('.')
 
 subdir('io')
 subdir('lib')
+subdir('log')
 subdir('stats')
 subdir('strategies')
 subdir('sleep_strategy')