diff --git a/emper/Runtime.cpp b/emper/Runtime.cpp
index bb5e1ef835d075194ed832e2ef663bac30103b7e..c246bbc6e0fffd1bd43b7208b8180c249117aacd 100644
--- a/emper/Runtime.cpp
+++ b/emper/Runtime.cpp
@@ -72,7 +72,9 @@ RuntimeStrategyFactory& Runtime::DEFAULT_STRATEGY =
 using emper::io::GlobalIoContext;
 using emper::io::IoContext;
 
-Runtime::Runtime(workerid_t workerCount, RuntimeStrategyFactory& strategyFactory, unsigned int seed)
+Runtime::Runtime(workerid_t workerCount, const std::vector<NewWorkerHook>& newWorkerHooks,
+								 bool pinWorkers, workerid_t pinningOffset, RuntimeStrategyFactory& strategyFactory,
+								 unsigned int seed)
 		: workerCount(workerCount),
 			workerLatch(workerCount),
 			firstWorkerThreadExitLatch(workerCount),
@@ -116,6 +118,9 @@ Runtime::Runtime(workerid_t workerCount, RuntimeStrategyFactory& strategyFactory
 		fromAnywhereStats = nullptr;
 	}
 
+	// transfere newWorkerHooks
+	for (const auto& f : newWorkerHooks) this->newWorkerHooks.push_back(f);
+
 	// initialize the global IoContext if a completer is used
 	if constexpr (emper::IO && emper::IO_COMPLETER_BEHAVIOR != emper::IoCompleterBehavior::none) {
 		// The global io_uring needs at least workerCount entries in its SQ because
@@ -130,21 +135,9 @@ Runtime::Runtime(workerid_t workerCount, RuntimeStrategyFactory& strategyFactory
 		}
 	}
 
-	bool pinWorkers = shouldPinWorkers();
-
-	// Core id we start the worker pinning
-	workerid_t pinningOffset = 0;
-	char* pinningOffsetEnv = std::getenv("EMPER_PINNING_OFFSET");
-	if (pinningOffsetEnv) {
-		if (!pinWorkers) {
-			DIE_MSG("EMPER_PIN_WORKERS=false and EMPER_PINNING_OFFSET are mutual exclusive");
-		}
-
-		int pinningOffsetInt = std::stoi(pinningOffsetEnv);
-		if (pinningOffsetInt > UINT8_MAX) {
-			DIE_MSG("Pinning offset " << pinningOffsetInt << " to big for its datatype");
-		}
-		pinningOffset = static_cast<workerid_t>(pinningOffsetInt);
+	// Check if the pinning settings are sound
+	if (pinningOffset && !pinWorkers) {
+		DIE_MSG("pinningOffset and not pinning workers are mutually exclusive");
 	}
 
 	for (workerid_t i = 0; i < workerCount; ++i) {
@@ -298,13 +291,9 @@ auto Runtime::workerLoop(Worker* worker) -> void* {
 }
 
 auto Runtime::getDefaultWorkerCount() -> workerid_t {
-	char* workerCountEnv = std::getenv("EMPER_WORKER_COUNT");
+	auto workerCountEnv = emper::lib::env::getUnsignedFromEnv<workerid_t>("EMPER_WORKER_COUNT");
 	if (workerCountEnv) {
-		int workerCountInt = std::stoi(workerCountEnv);
-		if (workerCountInt > UINT8_MAX) {
-			DIE_MSG("Worker count " << workerCountInt << " to big for its datatype");
-		}
-		return static_cast<workerid_t>(workerCountInt);
+		return workerCountEnv.value();
 	}
 
 	// The CPU count reported by sysconf(_SC_NPROCESSORS_ONLN), sysconf(_SC_NPROCESSORS_CONF)
diff --git a/emper/Runtime.hpp b/emper/Runtime.hpp
index c3467559ac55ea41dcc3cfa3166c0b394a1078d3..14ae319cee699fb8e0b2d58260f8dc33a91df07c 100644
--- a/emper/Runtime.hpp
+++ b/emper/Runtime.hpp
@@ -31,6 +31,7 @@
 #include "sleep_strategy/WorkerSleepStrategy.hpp"
 
 enum class LogSubsystem;
+class RuntimeBuilder;
 class ContextManager;
 class Dispatcher;
 class Fiber;
@@ -59,13 +60,16 @@ using emper::io::IoContext;
 using emper::sleep_strategy::WorkerSleepStrategy;
 
 class Runtime : public Logger<LogSubsystem::RUNTI> {
+ public:
+	using NewWorkerHook = std::function<void(workerid_t)>;
+
  private:
 	static std::mutex currentRuntimeMutex;
 	static Runtime* currentRuntime;
 
 	const workerid_t workerCount;
 
-	std::vector<std::function<void(workerid_t)>> newWorkerHooks;
+	std::vector<NewWorkerHook> newWorkerHooks;
 
 	Latch workerLatch;
 	Latch firstWorkerThreadExitLatch;
@@ -110,6 +114,10 @@ class Runtime : public Logger<LogSubsystem::RUNTI> {
 		return emper::lib::env::getBoolFromEnv("EMPER_PIN_WORKERS").value_or(true);
 	}
 
+	static auto getDefaultPinningOffset() -> workerid_t {
+		return emper::lib::env::getUnsignedFromEnv<workerid_t>("EMPER_PINNING_OFFSET").value_or(0);
+	}
+
  protected:
 	void addNewWorkerHook(const std::function<void(workerid_t)>& hook) {
 		newWorkerHooks.push_back(hook);
@@ -157,7 +165,12 @@ class Runtime : public Logger<LogSubsystem::RUNTI> {
 			: Runtime(getDefaultWorkerCount(), strategyFactory) {}
 
 	Runtime(workerid_t workerCount, RuntimeStrategyFactory& strategyFactory,
-					unsigned int seed = std::random_device()());
+					unsigned int seed = std::random_device()())
+			: Runtime(workerCount, std::vector<NewWorkerHook>(), shouldPinWorkers(),
+								getDefaultPinningOffset(), strategyFactory, seed) {}
+
+	Runtime(workerid_t workerCount, const std::vector<NewWorkerHook>& newWorkerHooks, bool pinWorkers,
+					workerid_t pinningOffset, RuntimeStrategyFactory& strategyFactory, unsigned int seed);
 
 	~Runtime();
 
@@ -216,6 +229,7 @@ class Runtime : public Logger<LogSubsystem::RUNTI> {
 	friend class AbstractWorkStealingScheduler;
 	template <LogSubsystem>
 	friend class Blockable;
+	friend RuntimeBuilder;
 	friend ContextManager;
 	friend Scheduler;
 	friend Dispatcher;
diff --git a/emper/RuntimeBuilder.hpp b/emper/RuntimeBuilder.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..5e3e8b2e7c535f50140319b0e5ef24ce8a7c467a
--- /dev/null
+++ b/emper/RuntimeBuilder.hpp
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright © 2021 Florian Fischer
+#pragma once
+
+#include <optional>
+#include <vector>
+
+#include "Runtime.hpp"
+#include "emper-common.h"
+
+class RuntimeBuilder {
+ private:
+	workerid_t workerCount = 0;
+
+	std::vector<Runtime::NewWorkerHook> newWorkerHooks;
+
+	RuntimeStrategyFactory* strategyFactory = nullptr;
+
+	std::optional<bool> pinWorkers = std::nullopt;
+	std::optional<workerid_t> pinningOffset = std::nullopt;
+
+	std::optional<unsigned> seed = std::nullopt;
+
+ public:
+	inline auto withWorkerCount(workerid_t workerCount) -> RuntimeBuilder& {
+		this->workerCount = workerCount;
+		return *this;
+	};
+
+	inline auto newWorkerHook(const Runtime::NewWorkerHook& hook) -> RuntimeBuilder& {
+		newWorkerHooks.push_back(hook);
+		return *this;
+	};
+
+	inline auto withPinWorkers(bool pinWorkers) -> RuntimeBuilder& {
+		this->pinWorkers = pinWorkers;
+		return *this;
+	};
+
+	inline auto withPinningOffset(workerid_t pinningOffset) -> RuntimeBuilder& {
+		this->pinningOffset = pinningOffset;
+		return *this;
+	};
+
+	inline auto withStrategyFactory(RuntimeStrategyFactory* factory) -> RuntimeBuilder& {
+		this->strategyFactory = factory;
+		return *this;
+	};
+
+	inline auto withSeed(unsigned seed) -> RuntimeBuilder& {
+		this->seed = seed;
+		return *this;
+	};
+
+	inline auto build() -> Runtime {
+		auto workerCount =
+				this->workerCount != 0 ? this->workerCount : Runtime::getDefaultWorkerCount();
+
+		auto* strategyFactory =
+				this->strategyFactory != nullptr ? this->strategyFactory : &Runtime::DEFAULT_STRATEGY;
+
+		auto pinWorkers = this->pinWorkers.value_or(Runtime::shouldPinWorkers());
+
+		auto pinningOffset = this->pinningOffset.value_or(Runtime::getDefaultPinningOffset());
+
+		auto seed = this->seed.value_or(std::random_device()());
+
+		return Runtime(workerCount, newWorkerHooks, pinWorkers, pinningOffset, *strategyFactory, seed);
+	}
+};
diff --git a/emper/lib/env.hpp b/emper/lib/env.hpp
index 65b6c4fc2dcefe3421286f64ff27ea1114de4709..4d97aaecba060ff8227b19d3930a336a50495e02 100644
--- a/emper/lib/env.hpp
+++ b/emper/lib/env.hpp
@@ -2,6 +2,7 @@
 // Copyright © 2021 Florian Fischer
 #pragma once
 
+#include <cinttypes>
 #include <string>
 
 #include "Debug.hpp"
@@ -27,4 +28,28 @@ static auto getBoolFromEnv(const std::string&& key) -> std::optional<bool> {
 	DIE_MSG(key << " has invalid value: " << envStr << " (expected true or false)");
 }
 
+template <typename unsigned_type>
+static auto getUnsignedFromEnv(const std::string&& key) -> std::optional<unsigned_type> {
+	DBG("parse " << key << " environment variable");
+	char* envVar = std::getenv(key.c_str());
+	if (!envVar) {
+		return std::nullopt;
+	}
+
+	std::string envStr(envVar);
+	char* last;
+
+	uintmax_t num = std::strtoumax(envStr.c_str(), &last, 10);
+	if (last != &envStr[0] + envStr.size()) {
+		DIE_MSG(key << " has invalid value: " << envStr << " (expected base-10 number)");
+	}
+
+	const unsigned_type t_max = std::numeric_limits<unsigned_type>::max();
+	if (num > static_cast<uintmax_t>(t_max)) {
+		DIE_MSG(key << " is to big: " << envStr << " (type '" << typeid(t_max).name()
+								<< "' max: " << t_max << ")");
+	}
+	return static_cast<unsigned_type>(num);
+}
+
 }	 // namespace emper::lib::env