diff --git a/CMakeLists.txt b/CMakeLists.txt
index e9e90abd03cf139703050ad1cf0886f426e3532f..aea9e94c9e7283a719c0a4c54892e952c452069b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -110,6 +110,7 @@ else()
 	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color=always -std=c++17 -fPIC -march=x86-64 -mfpmath=sse -Wall -Werror=unused-result -Werror=return-type")
 	set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -pg")
 	set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
+	set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -O3")
 	set(OS_LIBS "dl")
 endif()
 
diff --git a/src/universe.cpp b/src/universe.cpp
index 539d3650680bd3d24a3640205a82ecbb56e215e0..7858a1a3d5f2c901a3aee1fd3a937d1b38871fa4 100644
--- a/src/universe.cpp
+++ b/src/universe.cpp
@@ -172,6 +172,12 @@ void Universe::shutdown() {
 
 	active_ = false;
 	thread_.join();
+
+	// FIXME: This shouldn't be needed
+	while (peer_instances_ > 0 && ftl::pool.size() > 0) {
+		LOG(INFO) << "waiting for peers to destroy...";
+		std::this_thread::sleep_for(std::chrono::milliseconds(2));
+	}
 }
 
 bool Universe::listen(const ftl::URI &addr) {
@@ -291,8 +297,6 @@ socket_t Universe::_setDescriptors() {
 		}
 	}
 
-	// FIXME: Bug, it crashes here sometimes, segfault on reading the shared_ptr
-
 	//Set the file descriptors for each client
 	for (const auto &s : peers_) {
 		// NOTE: s->isValid() should return true only and only if a valid OS
@@ -386,7 +390,7 @@ void Universe::_periodic() {
 		std::string addr = i->peer->getURI();
 
 		{
-			UNIQUE_LOCK(net_mutex_,lk);
+			SHARED_LOCK(net_mutex_,lk);
 			ftl::URI u(addr);
 			bool removed = false;
 
@@ -452,8 +456,7 @@ void Universe::_run() {
 		// It is an error to use "select" with no sockets ... so just sleep
 		if (n == 0) {
 			std::shared_lock lk(net_mutex_);
-			socket_cv_.wait_for(lk, std::chrono::milliseconds(300), [this](){ return listeners_.size() > 0 || connection_count_ > 0; });
-			//std::this_thread::sleep_for(std::chrono::milliseconds(100));
+			socket_cv_.wait_for(lk, std::chrono::milliseconds(100), [this](){ return listeners_.size() > 0 || connection_count_ > 0; });
 			continue;
 		}
 
@@ -506,7 +509,7 @@ void Universe::_run() {
 		for (size_t p=0; p<peers_.size(); ++p) {
 			auto s = peers_[(p+phase_)%peers_.size()];
 
-			if (s != NULL && s->isValid()) {
+			if (s && s->isValid()) {
 				// Note: It is possible that the socket becomes invalid after check but before
 				// looking at the FD sets, therefore cache the original socket
 				SOCKET sock = s->_socket();
@@ -514,13 +517,17 @@ void Universe::_run() {
 
 				if (FD_ISSET(sock, &impl_->sfderror_)) {
 					if (s->socketError()) {
+						//lk.unlock();
 						s->close();
+						//lk.lock();
 						continue;  // No point in reading data...
 					}
 				}
 				//If message received from this client then deal with it
 				if (FD_ISSET(sock, &impl_->sfdread_)) {
+					//lk.unlock();
 					s->data();
+					//lk.lock();
 				}
 			}
 		}
diff --git a/src/universe.hpp b/src/universe.hpp
index 9b8fb0dc4c8c3e93a7d5428f7cd6191f15a93c27..0f8d99943fbcf00fe28fc2ab22dc3d979c9397af 100644
--- a/src/universe.hpp
+++ b/src/universe.hpp
@@ -205,8 +205,8 @@ private:
 	std::list<std::shared_ptr<ftl::net::Peer>> garbage_;
 	ftl::Handle garbage_timer_;
 
-	size_t send_size_;
-	size_t recv_size_;
+	// size_t send_size_;
+	// size_t recv_size_;
 	double periodic_time_;
 	int reconnect_attempts_;
 	std::atomic_int connection_count_ = 0;	// Active connections
@@ -236,7 +236,7 @@ void Universe::bind(const std::string &name, F func) {
 template <typename... ARGS>
 void Universe::broadcast(const std::string &name, ARGS... args) {
 	SHARED_LOCK(net_mutex_,lk);
-	for (auto p : peers_) {
+	for (auto &p : peers_) {
 		if (!p->waitConnection()) continue;
 		p->send(name, args...);
 	}
@@ -265,7 +265,7 @@ std::optional<R> Universe::findOne(const std::string &name, ARGS... args) {
 
 	{
 		SHARED_LOCK(net_mutex_,lk);
-		for (auto p : peers_) {
+		for (auto &p : peers_) {
 			if (!p->waitConnection()) continue;
 			p->asyncCall<std::optional<R>>(name, handler, args...);
 		}
@@ -302,7 +302,7 @@ std::vector<R> Universe::findAll(const std::string &name, ARGS... args) {
 
 	{
 		SHARED_LOCK(net_mutex_,lk);
-		for (auto p : peers_) {
+		for (auto &p : peers_) {
 			if (!p->waitConnection()) continue;
 			++sdata->sentcount;
 			p->asyncCall<std::vector<R>>(name, handler, args...);
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index f6dd15f3c01fe2f3c61236f6b0c6408b3db2d8fc..3777c230bdcc33d0c4ac980089ace4aec46cc2fe 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -39,3 +39,13 @@ target_link_libraries(net_integration beyond-protocol
 	${URIPARSER_LIBRARIES})
 
 add_test(NetIntegrationTest net_integration)
+
+### Net performance ############################################################
+add_executable(net_performance
+	$<TARGET_OBJECTS:CatchTestFTL>
+	./net_performance.cpp)
+target_include_directories(net_performance PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}../include")
+target_link_libraries(net_performance beyond-protocol
+	GnuTLS::GnuTLS Threads::Threads ${UUID_LIBRARIES} ${URIPARSER_LIBRARIES} ${OS_LIBS})
+
+add_test(NetPerformance net_performance)
diff --git a/test/net_performance.cpp b/test/net_performance.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1472746f3936f939a86c18ab46c84580ef8708c5
--- /dev/null
+++ b/test/net_performance.cpp
@@ -0,0 +1,112 @@
+#include "catch.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include "../src/universe.hpp"
+#include "../src/peer.hpp"
+#include "../src/protocol.hpp"
+
+#include <thread>
+#include <chrono>
+
+#include "../src/protocol/connection.hpp"
+#include "../src/protocol/tcp.hpp"
+
+// msgpack uses binary format when DTYPE is char
+typedef char DTYPE;
+constexpr int BSIZE = (1 << 23); // bytes in each send (2^20 = 1 MiB)
+constexpr int COUNT = 100; // how many times send called
+
+using namespace ftl::net;
+
+static std::vector<DTYPE> data_test;
+static std::atomic_uint64_t recv_cnt_ = 0;
+static auto t_last_recv_ = std::chrono::steady_clock::now();
+
+static void recv_data(std::vector<DTYPE> data) {
+	recv_cnt_.fetch_add(data.size() * sizeof(DTYPE));
+	t_last_recv_ = std::chrono::steady_clock::now();
+}
+
+static float peer_send(ftl::net::Peer* p, const std::vector<DTYPE>& data, int cnt) {
+	auto t_start = std::chrono::steady_clock::now();
+	auto t_stop = std::chrono::steady_clock::now();
+
+	size_t bytes_sent = 0;
+	size_t bytes = data.size() * sizeof(DTYPE);
+
+	for (int i = 0; i < cnt; i++) {
+		p->send("recv_data", data);
+		bytes_sent += bytes;
+	}
+
+	t_stop = std::chrono::steady_clock::now();
+
+	// should be ok, since blocking sockets are used
+	float ms = std::chrono::duration_cast<std::chrono::milliseconds>
+					(t_stop - t_start).count();
+	float throughput_send =  (float(bytes_sent >> 20)/ms)*1000.0f*8.0f;
+
+	LOG(INFO) << "sent " << (bytes_sent >> 20) << " MiB in " << ms << " ms, "
+			  << "connection throughput: "
+			  << throughput_send << " MBit/s";
+
+	ms = std::chrono::duration_cast<std::chrono::milliseconds>
+					(t_last_recv_ - t_start).count();
+	float throughput_recv =  (float(recv_cnt_ >> 20)/ms)*1000.0f*8.0f;
+
+	LOG(INFO) << "received " << (bytes_sent >> 20) << " MiB in " << ms << " ms, "
+			  << "connection throughput: "
+			  << throughput_recv << " MBit/s";
+
+	recv_cnt_ = 0;
+
+	return (throughput_send + throughput_recv)/2.0f;
+}
+
+std::unique_ptr<Universe> net_server;
+std::unique_ptr<Universe> net_client;
+std::atomic_bool init_ = false;
+
+static void init() {
+	if (init_.exchange(true)) { return; }
+
+	net_server = std::make_unique<Universe>();
+	net_server->setLocalID(ftl::UUID());
+	net_client = std::make_unique<Universe>();
+	net_client->setLocalID(ftl::UUID());
+
+	net_server->bind("test_server", [](){ LOG(INFO) << "test_server"; });
+	net_client->bind("test_client", [](){ LOG(INFO) << "test_client"; });
+	net_server->bind("recv_data", recv_data);
+
+	data_test.clear(); data_test.reserve(BSIZE);
+	for (int i = 0; i < BSIZE; i++) { data_test.push_back(i ^ (i - 1)); }
+}
+
+ftl::URI uri("");
+
+TEST_CASE("throughput", "[net]") {
+
+	SECTION("create server and client Universe instances") {
+		init();
+	}
+
+	SECTION("create TCP server") {
+		std::string host = "localhost";
+		int port = 0; // pick random port
+		net_server->listen(ftl::URI("tcp://" + host + ":" + std::to_string(port)));
+		int listening_port = net_server->getListeningURIs()[0].getPort();
+		uri = ftl::URI("tcp://localhost:" + std::to_string(listening_port));
+	}
+
+	SECTION("TCP throughput") {
+		LOG(INFO) << "connecting to " << uri.to_string();
+		auto p = net_client->connect(uri);
+
+		while(!p->isConnected()) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); }
+
+		auto r = peer_send(p.get(), data_test, COUNT);
+		//REQUIRE(r > 8000);
+	}
+}
\ No newline at end of file
diff --git a/test/peer_unit.cpp b/test/peer_unit.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e0407614779ba501601284b470c08d9d48f764c9
--- /dev/null
+++ b/test/peer_unit.cpp
@@ -0,0 +1,455 @@
+/** Peer unit test. Does not test lower level protocols (TLS/WebSocket) */
+
+#include "catch.hpp"
+
+#include <iostream>
+#include <memory>
+
+#include <vector>
+#include <tuple>
+#include <thread>
+#include <chrono>
+#include <functional>
+#include <sstream>
+
+#include <ftl/net/common.hpp>
+#include <ftl/net/peer.hpp>
+#include <ftl/net/protocol.hpp>
+#include <ftl/config.h>
+
+#include "../src/socket.hpp"
+#include "../src/protocol/connection.hpp"
+
+#define _FTL_NET_UNIVERSE_HPP_
+
+using std::tuple;
+using std::get;
+using std::vector;
+using ftl::net::Peer;
+using std::this_thread::sleep_for;
+using std::chrono::milliseconds;
+
+// --- Mock --------------------------------------------------------------------
+
+namespace ftl {
+namespace net {
+
+typedef unsigned int callback_t;
+
+class Universe {
+	public:
+	Universe() {};
+
+	ftl::UUID id() { return ftl::UUID(); }
+
+	void _notifyConnect(Peer*) {}
+	void _notifyDisconnect(Peer*) {}
+	void removeCallback(callback_t id) {}
+
+	callback_t onConnect(const std::function<void(Peer*)> &f) { return 0; }
+	callback_t onDisconnect(const std::function<void(Peer*)> &f) { return 0; }
+
+	size_t getSendBufferSize(ftl::URI::scheme_t s) const { return 10*1024; }
+	size_t getRecvBufferSize(ftl::URI::scheme_t s) const { return 10*1024; }
+};
+}
+}
+
+using ftl::net::internal::Socket;
+using ftl::net::internal::SocketConnection;
+
+// Mock connection, reads/writes from fakedata
+// TODO: use separate in/out data
+static std::map<int, std::string> fakedata;
+//static std::mutex fakedata_mtx;
+
+class Connection_Mock : public SocketConnection {
+public:
+	const int id_;
+	Connection_Mock(int id) : SocketConnection(), id_(id) {
+
+	}
+
+	void connect(const ftl::URI&, int) override {}
+
+	ssize_t send(const char* buffer, size_t len) override {
+		fakedata[id_] += std::string(buffer, len);
+		return len;
+	}
+	
+	ssize_t recv(char *buffer, size_t len) override {
+		if (fakedata.count(id_) == 0) {
+			// this is an error in test
+			std::cout << "unrecognised socket, test error (FIXME)" << std::endl;
+			return 0;
+		}
+
+		size_t l = fakedata[id_].size();
+		CHECK(l <= len); // FIXME: buffer overflow
+		std::memcpy(buffer, fakedata[id_].c_str(), l);
+		
+		fakedata.erase(id_);
+
+		return l;
+	}
+
+	ssize_t writev(const struct iovec *iov, int iovcnt) override {
+		size_t sent = 0;
+		std::stringstream ss;
+		for (int i = 0; i < iovcnt; i++) {
+			ss << std::string((char*)(iov[i].iov_base), size_t(iov[i].iov_len));
+			sent += iov[i].iov_len;
+		}
+		fakedata[id_] += ss.str();
+		return sent;
+	}
+};
+
+static std::atomic<int> ctr_ = 0;
+std::unique_ptr<SocketConnection> ftl::net::internal::createConnection(const ftl::URI &uri) {
+	return std::make_unique<Connection_Mock>(ctr_++);
+}
+
+bool ftl::net::internal::Socket::is_fatal() { 
+	return false;
+}
+
+class MockPeer : public Peer {
+private:
+	MockPeer(int) :
+		Peer(ftl::net::internal::createConnection(ftl::URI("")), new ftl::net::Universe()),
+		idx(ctr_ - 1) {}
+	
+	MockPeer(std::string uri) :
+		Peer(ftl::URI(""), new ftl::net::Universe()), idx(ctr_ - 1) {}
+
+public:
+	int idx;
+	
+	void mock_data() { data(); }
+
+	static MockPeer create_connecting_peer() {
+		return MockPeer("");
+	};
+	
+	static MockPeer create_listening_peer() {
+		return MockPeer(0);
+	};
+};
+
+// --- Support -----------------------------------------------------------------
+
+void send_handshake(Peer &p) {
+	ftl::UUID id;
+	p.send("__handshake__", ftl::net::kMagic, ((8 << 16) + (5 << 8) + 2), id);
+}
+
+template <typename T>
+tuple<std::string, T> readResponse(int s) {
+	msgpack::object_handle msg = msgpack::unpack(fakedata[s].data(), fakedata[s].size());
+	tuple<uint8_t, std::string, T> req;
+	msg.get().convert(req);
+	return std::make_tuple(get<1>(req), get<2>(req));
+}
+
+template <typename T>
+tuple<uint32_t, T> readRPC(int s) {
+	msgpack::object_handle msg = msgpack::unpack(fakedata[s].data(), fakedata[s].size());
+	tuple<uint8_t, uint32_t, std::string, T> req;
+	msg.get().convert(req);
+	return std::make_tuple(get<1>(req), get<3>(req));
+}
+
+template <typename T>
+T readRPCReturn(int s) {
+	msgpack::object_handle msg = msgpack::unpack(fakedata[s].data(), fakedata[s].size());
+	tuple<uint8_t, uint32_t, std::string, T> req;
+	msg.get().convert(req);
+	return get<3>(req);
+}
+
+// --- Files to test -----------------------------------------------------------
+
+#include "../src/peer.cpp"
+
+// --- Tests -------------------------------------------------------------------
+
+TEST_CASE("Peer(int)", "[]") {
+	SECTION("initiates a valid handshake") {
+		MockPeer s = MockPeer::create_listening_peer();
+
+		auto [name, hs] = readResponse<ftl::net::Handshake>(0);
+		
+		REQUIRE( name == "__handshake__" );
+		
+		// 1) Sends magic (64bits)
+		REQUIRE( get<0>(hs) == ftl::net::kMagic );
+		 
+		// 2) Sends FTL Version
+		REQUIRE( get<1>(hs) == (FTL_VERSION_MAJOR << 16) + (FTL_VERSION_MINOR << 8) + FTL_VERSION_PATCH );
+		
+		// 3) Sends peer UUID
+		
+		
+		REQUIRE( s.status() == Peer::kConnecting );
+	}
+	
+	SECTION("completes on full handshake") {
+		MockPeer s_l = MockPeer::create_listening_peer();
+		MockPeer s_c = MockPeer::create_connecting_peer();
+		
+		// get sent message by s_l and place it in s_c's buffer
+		fakedata[s_c.idx] = fakedata[s_l.idx]; 
+		s_l.mock_data(); // listenin peer: process
+		// vice versa, listening peer gets reply and processes it
+		fakedata[s_l.idx] = fakedata[s_c.idx]; 
+		s_c.mock_data(); // connecting peer: process
+		sleep_for(milliseconds(50));
+
+		// both peers should be connected now
+		REQUIRE( s_c.status() == Peer::kConnected );
+		REQUIRE( s_l.status() == Peer::kConnected );
+	}
+	
+	SECTION("has correct version on full handshake") {
+		
+		// MockPeer s = MockPeer::create_connecting_peer();
+		// Send handshake response
+		// send_handshake(s);
+		// s.mock_data();
+		//sleep_for(milliseconds(50));
+		// REQUIRE( (s.getFTLVersion() ==  (8 << 16) + (5 << 8) + 2) );
+	}
+	
+	SECTION("has correct peer id on full handshake") {
+		// MockPeer s = MockPeer::create_connecting_peer();
+		//
+		// Send handshake response
+		//REQUIRE( s.id() ==   );
+	}
+}
+
+TEST_CASE("Peer::call()", "[rpc]") {
+	MockPeer s = MockPeer::create_connecting_peer();
+	send_handshake(s);
+	s.mock_data();
+	sleep_for(milliseconds(50));
+	
+	SECTION("one argument call") {
+		REQUIRE( s.isConnected() );
+		
+		fakedata[s.idx] = "";
+		
+		// Thread to provide response to otherwise blocking call
+		std::thread thr([&s]() {
+			while (fakedata[s.idx].size() == 0) std::this_thread::sleep_for(std::chrono::milliseconds(20));
+			
+			auto [id,value] = readRPC<tuple<int>>(s.idx);
+			auto res_obj = std::make_tuple(1,id,"__return__",get<0>(value)+22);
+			std::stringstream buf;
+			msgpack::pack(buf, res_obj);
+			fakedata[s.idx] = buf.str();
+			s.mock_data();
+			sleep_for(milliseconds(50));
+		});
+		int res = s.call<int>("test1", 44);
+		thr.join();
+		
+		REQUIRE( (res == 66) );
+	}
+	
+	SECTION("no argument call") {
+		REQUIRE( s.isConnected() );
+		
+		fakedata[s.idx] = "";
+		
+		// Thread to provide response to otherwise blocking call
+		std::thread thr([&s]() {
+			while (fakedata[s.idx].size() == 0) std::this_thread::sleep_for(std::chrono::milliseconds(20));
+			
+			auto res = readRPC<tuple<>>(s.idx);
+			auto res_obj = std::make_tuple(1,std::get<0>(res),"__return__",77);
+			std::stringstream buf;
+			msgpack::pack(buf, res_obj);
+			fakedata[s.idx] = buf.str();
+			s.mock_data();
+			sleep_for(milliseconds(50));
+		});
+		
+		int res = s.call<int>("test1");
+		
+		thr.join();
+		
+		REQUIRE( (res == 77) );
+	}
+
+	SECTION("vector return from call") {
+		REQUIRE( s.isConnected() );
+		
+		fakedata[s.idx] = "";
+		
+		// Thread to provide response to otherwise blocking call
+		std::thread thr([&s]() {
+			while (fakedata[s.idx].size() == 0) std::this_thread::sleep_for(std::chrono::milliseconds(20));
+			
+			auto res = readRPC<tuple<>>(s.idx);
+			vector<int> data = {44,55,66};
+			auto res_obj = std::make_tuple(1,std::get<0>(res),"__return__",data);
+			std::stringstream buf;
+			msgpack::pack(buf, res_obj);
+			fakedata[s.idx] = buf.str();
+			s.mock_data();
+			sleep_for(milliseconds(50));
+		});
+		
+		vector<int> res = s.call<vector<int>>("test1");
+		
+		thr.join();
+		
+		REQUIRE( (res[0] == 44) );
+		REQUIRE( (res[2] == 66) );
+	}
+}
+
+TEST_CASE("Peer::bind()", "[rpc]") {
+	MockPeer s = MockPeer::create_listening_peer();
+	send_handshake(s);
+	s.mock_data();
+	sleep_for(milliseconds(50));
+	
+
+	SECTION("no argument call") {
+		bool done = false;
+		
+		s.bind("hello", [&]() {
+			done = true;
+		});
+
+		s.send("hello");
+		s.mock_data(); // Force it to read the fake send...
+		sleep_for(milliseconds(50));
+		
+		REQUIRE( done );
+	}
+	
+	SECTION("one argument call") {		
+		int done = 0;
+		
+		s.bind("hello", [&](int a) {
+			done = a;
+		});
+		
+		s.send("hello", 55);
+		s.mock_data(); // Force it to read the fake send...
+		sleep_for(milliseconds(50));
+		
+		REQUIRE( (done == 55) );
+	}
+	
+	SECTION("two argument call") {		
+		std::string done;
+		
+		s.bind("hello", [&](int a, std::string b) {
+			done = b;
+		});
+
+		s.send("hello", 55, "world");
+		s.mock_data(); // Force it to read the fake send...
+		sleep_for(milliseconds(50));
+		
+		REQUIRE( (done == "world") );
+	}
+
+	SECTION("int return value") {		
+		int done = 0;
+		
+		s.bind("hello", [&](int a) -> int {
+			done = a;
+			return a;
+		});
+		
+		s.asyncCall<int>("hello", [](int a){}, 55);
+		s.mock_data(); // Force it to read the fake send...
+		sleep_for(milliseconds(50));
+		
+		REQUIRE( (done == 55) );
+		REQUIRE( (readRPCReturn<int>(s.idx) == 55) );
+	}
+
+	SECTION("vector return value") {		
+		int done = 0;
+		
+		s.bind("hello", [&](int a) -> vector<int> {
+			done = a;
+			vector<int> b = {a,45};
+			return b;
+		});
+		
+		s.asyncCall<int>("hello", [](int a){}, 55);
+		s.mock_data(); // Force it to read the fake send...
+		sleep_for(milliseconds(50));
+		
+		REQUIRE( (done == 55) );
+
+		auto res = readRPCReturn<vector<int>>(s.idx);
+		REQUIRE( (res[1] == 45) );
+	}
+}
+
+TEST_CASE("Socket::send()", "[io]") {
+	MockPeer s = MockPeer::create_connecting_peer();
+	sleep_for(milliseconds(50));
+
+	SECTION("send an int") {
+		int i = 607;
+		
+		s.send("dummy",i);
+		
+		auto [name, value] = readResponse<tuple<int>>(s.idx);
+		
+		REQUIRE( (name == "dummy") );
+		REQUIRE( (get<0>(value) == 607) );
+	}
+	
+	SECTION("send a string") {
+		std::string str("hello world");
+		s.send("dummy",str);
+		
+		auto [name, value] = readResponse<tuple<std::string>>(s.idx);
+		
+		REQUIRE( (name == "dummy") );
+		REQUIRE( (get<0>(value) == "hello world") );
+	}
+	
+	SECTION("send const char* string") {
+		s.send("dummy","hello world");
+		
+		auto [name, value] = readResponse<tuple<std::string>>(s.idx);
+		
+		REQUIRE( (name == "dummy") );
+		REQUIRE( (get<0>(value) == "hello world") );
+	}
+	
+	SECTION("send a tuple") {
+		auto tup = std::make_tuple(55,66,true,6.7);
+		s.send("dummy",tup);
+		
+		auto [name, value] = readResponse<tuple<decltype(tup)>>(s.idx);
+		
+		REQUIRE( (name == "dummy") );
+		REQUIRE( (get<1>(get<0>(value)) == 66) );
+	}
+	
+	SECTION("send multiple strings") {
+		std::string str("hello ");
+		std::string str2("world");
+		s.send("dummy2",str,str2);
+		
+		auto [name, value] = readResponse<tuple<std::string,std::string>>(s.idx);
+		
+		REQUIRE( (name == "dummy2") );
+		REQUIRE( (get<0>(value) == "hello ") );
+		REQUIRE( (get<1>(value) == "world") );
+	}
+}
+
diff --git a/test/socket_mock.cpp b/test/socket_mock.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b9e71e893afe951c1c91d92ac4b8db2c7155a73a
--- /dev/null
+++ b/test/socket_mock.cpp
@@ -0,0 +1,114 @@
+/** no-op socket for unit tests. Simulated data transfer implemented in
+ * Connection_Mock */
+
+#include "../src/socket.hpp"
+
+#include <string>
+
+using ftl::net::internal::Socket;
+using ftl::net::internal::SocketAddress;
+
+bool ftl::net::internal::resolve_inet_address(const std::string &hostname, int port, SocketAddress &address) {
+	return true;
+}
+
+Socket::Socket(int domain, int type, int protocol) :
+		status_(STATUS::UNCONNECTED), fd_(-1), family_(domain) {
+}
+
+bool Socket::is_valid() { return true; }
+
+bool Socket::is_open() { return true; }
+
+ssize_t Socket::recv(char *buffer, size_t len, int flags) {
+	return 0;
+}
+
+ssize_t Socket::send(const char* buffer, size_t len, int flags) {
+	return 0;
+}
+
+ssize_t Socket::writev(const struct iovec *iov, int iovcnt) {
+	return 0;
+}
+
+int Socket::bind(const SocketAddress &addr) {
+	return 0;
+}
+
+int Socket::listen(int backlog) {
+	return 0;
+}
+
+Socket Socket::accept(SocketAddress &addr) {
+	return Socket();
+}
+
+int Socket::connect(const SocketAddress& address) {
+	return 0;
+}
+
+int Socket::connect(const SocketAddress &address, int timeout) {
+	return 0;
+}
+
+/// Close socket (if open). Multiple calls are safe.
+bool Socket::close() {
+	return true;
+}
+
+int Socket::setsockopt(int level, int optname, const void *optval, socklen_t optlen) {
+	return 0;
+}
+
+int Socket::getsockopt(int level, int optname, void *optval, socklen_t *optlen) {
+	return 0;
+}
+
+bool Socket::set_recv_buffer_size(size_t sz) {
+	return true;
+}
+
+bool Socket::set_send_buffer_size(size_t sz) {
+	return true;
+}
+
+size_t Socket::get_recv_buffer_size() {
+	return 0;
+}
+
+size_t Socket::get_send_buffer_size() {
+	return 0;
+}
+
+
+void Socket::set_blocking(bool val) {	
+}
+
+
+bool Socket::is_blocking() {
+	return true;
+}
+
+std::string Socket::get_error_string() {
+	return "not real socket";
+}
+
+bool Socket::set_nodelay(bool val) {
+	return true;
+}
+
+SocketAddress Socket::getsockname() {
+	SocketAddress addr;
+	return addr;
+}
+
+// TCP socket
+
+Socket ftl::net::internal::create_tcp_socket() {
+	return Socket();
+}
+
+std::string ftl::net::internal::get_ip(SocketAddress& address) { return ""; }
+std::string ftl::net::internal::get_host(SocketAddress& address) { return ""; }
+int ftl::net::internal::get_port(SocketAddress& address) { return 0; } 
\ No newline at end of file