diff --git a/Makefile b/Makefile
index 5081a62e7be743da695d472c71ac761eb2d69953..59f34ce7ca241d4ad38e71e6a71574ecc851b0a5 100644
--- a/Makefile
+++ b/Makefile
@@ -102,6 +102,10 @@ fix-includes: all
 stresstest: test
 	./stresstest/stresstest.sh build/tests/simplest_fib_test
 
+PHONY: test-echo
+test-echo:
+	./tools/test-echo-server-and-client
+
 # TODO: Determine how we can run also jobs from the 'test' stage,
 # e.g. test-gcc.
 .PHONY: gitlab-runner
diff --git a/tools/test-echo-server-and-client b/tools/test-echo-server-and-client
new file mode 100755
index 0000000000000000000000000000000000000000..41a215211daa9887cafd448d2b980c9c30ab4dda
--- /dev/null
+++ b/tools/test-echo-server-and-client
@@ -0,0 +1,101 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-3.0-or-later
+# Copyright © 2021 Florian Schmaus
+set -euo pipefail
+
+SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
+ROOTDIR="$(readlink -f "${SCRIPTDIR}/..")"
+BUILDDIR="${ROOTDIR}/build"
+
+echoerr() { echo "$@" 1>&2; }
+
+PRESERVE_TMPDIR=false
+while getopts :dp OPT; do
+	case $OPT in
+		d)
+			PRESERVE_TMPDIR=true
+			set -x
+			;;
+		p)
+			PRESERVE_TMPDIR=true
+			;;
+		*)
+			print "usage: ${0##*/} [-d] [--] ARGS..."
+			exit 2
+	esac
+done
+shift $(( OPTIND - 1 ))
+OPTIND=1
+
+if [[ ! -L "${BUILDDIR}" ]]; then
+	make -C "${ROOTDIR}"
+fi
+
+TMPDIR=$(mktemp --directory --tmpdir=/var/tmp emper-echo-test.XXXX)
+cleanup() {
+	if ! $PRESERVE_TMPDIR; then
+		rm -rf "${TMPDIR}"
+	fi
+
+	# Ensure that we don't leave a stray echoserver
+	if pgrep --count echoserver > /dev/null; then
+		killall echoserver
+	fi
+}
+trap cleanup EXIT
+
+echo "Starting echo client/server test. Using ${TMPDIR} for logs"
+
+readonly ECHO_CLIENT="${BUILDDIR}/apps/echoclient"
+readonly ECHO_SERVER="${BUILDDIR}/apps/echoserver"
+
+run_echo_client_and_server() {
+	local -r run_name="${1}"
+	local -r echo_client_opts="${2-}"
+
+	local -r logdir="${TMPDIR}/${run_name}"
+	mkdir "${logdir}"
+
+	local -r echo_server_pidfile="${logdir}/echo_server.pid"
+
+	echo "Performing echo client/server ${run_name} run. Logdir: ${logdir}"
+
+	"${ECHO_SERVER}" \
+		> "${logdir}/server.log" \
+		2> "${logdir}/server.err" &
+	echo "${!}" >> "${echo_server_pidfile}"
+
+	# Wait till the echo server port becomes accessible.
+	# https://stackoverflow.com/a/50055449/194894
+	timeout 30 bash -c \
+		'until printf "" 2>>/dev/null >>/dev/tcp/${0}/${1}; do sleep 1; done' \
+		localhost 12345
+
+	set +e
+	# shellcheck disable=SC2086
+	"${ECHO_CLIENT}" \
+		 ${echo_client_opts} \
+		 > "${logdir}/client.log" \
+		 2> "${logdir}/client.err"
+	local echo_client_ret="${?}"
+	set -e
+
+	local echo_server_pid
+	echo_server_pid=$(cat "${echo_server_pidfile}")
+	if ps -p "${echo_server_pid}" > /dev/null; then
+		# TODO: Re-enable this warning once the client is sending a
+		# 'quit' once it is finished.
+#		echoerr "WARNING: echo server was not terminated by client"
+		kill "${echo_server_pid}"
+	fi
+
+	if [[ "${echo_client_ret}" -ne 0 ]]; then
+		echoerr "ERROR: Echo client exited with ${echo_client_ret}"
+		PRESERVE_TMPDIR=true
+		exit 1
+	fi
+}
+
+run_echo_client_and_server "quicktest" "-c 10 -i 10"
+
+run_echo_client_and_server "load-switch" "-c 100 -i 1000 --max-low-load-clients 50 --load-switch-period-ms 250"