diff --git a/Knot.files b/Knot.files
index 5f5370a1ff29474eac56d5aee38463f3c8a17c78..5d2df54469c3dd44d34f945d38de216ec78567fd 100644
--- a/Knot.files
+++ b/Knot.files
@@ -6,6 +6,8 @@ src/contrib/base64.h
 src/contrib/base64url.c
 src/contrib/base64url.h
 src/contrib/color.h
+src/contrib/conn_pool.c
+src/contrib/conn_pool.h
 src/contrib/ctype.h
 src/contrib/dnstap/convert.c
 src/contrib/dnstap/convert.h
diff --git a/doc/man/knot.conf.5in b/doc/man/knot.conf.5in
index 1b0c746e39ea809ba32330a667116bf6cc832cd8..cac59ff779ed64b34278ab2e697c62d288b621b6 100644
--- a/doc/man/knot.conf.5in
+++ b/doc/man/knot.conf.5in
@@ -191,6 +191,8 @@ server:
     tcp\-max\-clients: INT
     tcp\-reuseport: BOOL
     tcp\-fastopen: BOOL
+    remote\-pool\-limit: INT
+    remote\-pool\-timeout: TIME
     socket\-affinity: BOOL
     udp\-max\-payload: SIZE
     udp\-max\-payload\-ipv4: SIZE
@@ -358,6 +360,21 @@ is \fB1\fP for server side, and \fBnet.inet.tcp.fastopen.client_enable\fP is
 .UNINDENT
 .sp
 \fIDefault:\fP off
+.SS remote\-pool\-limit
+.sp
+If nonzero, the server will keep up to this number of outgoing TCP connections
+open for later use. This is an optimization to avoid frequent opening of
+TCP connections to the same remote.
+.sp
+Change of this parameter requires restart of the Knot server to take effect.
+.sp
+\fIDefault:\fP 0
+.SS remote\-pool\-timeout
+.sp
+The timeout in seconds after which the unused kept\-open outgoing TCP connections
+to remote servers are closed.
+.sp
+\fIDefault:\fP 5
 .SS socket\-affinity
 .sp
 If enabled and if SO_REUSEPORT is available on Linux, all configured network
diff --git a/doc/reference.rst b/doc/reference.rst
index 63542210e9f622fd6c5d7802fffc4f02f0d2f5ca..2cea76fffc9969c7a356623bfcc0179b45188da1 100644
--- a/doc/reference.rst
+++ b/doc/reference.rst
@@ -142,6 +142,8 @@ General options related to the server.
      tcp-max-clients: INT
      tcp-reuseport: BOOL
      tcp-fastopen: BOOL
+     remote-pool-limit: INT
+     remote-pool-timeout: TIME
      socket-affinity: BOOL
      udp-max-payload: SIZE
      udp-max-payload-ipv4: SIZE
@@ -351,6 +353,29 @@ configuration as it's enabled automatically if supported by OS.
 
 *Default:* off
 
+.. _server_remote-pool-limit:
+
+remote-pool-limit
+-----------------
+
+If nonzero, the server will keep up to this number of outgoing TCP connections
+open for later use. This is an optimization to avoid frequent opening of
+TCP connections to the same remote.
+
+Change of this parameter requires restart of the Knot server to take effect.
+
+*Default:* 0
+
+.. _server_remote-pool-timeout:
+
+remote-pool-timeout
+-------------------
+
+The timeout in seconds after which the unused kept-open outgoing TCP connections
+to remote servers are closed.
+
+*Default:* 5
+
 .. _server_socket-affinity:
 
 socket-affinity
diff --git a/src/contrib/Makefile.inc b/src/contrib/Makefile.inc
index ee987071d893ccda78f9d7fe2869159269a1e2e4..32fb30750c5c602ea088c141907b768f1f1641ef 100644
--- a/src/contrib/Makefile.inc
+++ b/src/contrib/Makefile.inc
@@ -29,6 +29,8 @@ libcontrib_la_SOURCES = \
 	contrib/base64.h			\
 	contrib/base64url.c			\
 	contrib/base64url.h			\
+	contrib/conn_pool.c			\
+	contrib/conn_pool.h			\
 	contrib/color.h				\
 	contrib/ctype.h				\
 	contrib/files.c				\
diff --git a/src/contrib/conn_pool.c b/src/contrib/conn_pool.c
new file mode 100644
index 0000000000000000000000000000000000000000..cb8af8255d79896a2a1e86ac2baa3e8bd56947ec
--- /dev/null
+++ b/src/contrib/conn_pool.c
@@ -0,0 +1,253 @@
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "contrib/conn_pool.h"
+
+#include "contrib/sockaddr.h"
+
+conn_pool_t *global_conn_pool = NULL;
+
+static void *closing_thread(void *_arg)
+{
+	conn_pool_t *pool = _arg;
+
+	while (true) {
+		knot_time_t now = knot_time(), next = 0;
+		knot_timediff_t timeout = conn_pool_timeout(pool, 0);
+		assert(timeout != 0);
+
+		while (true) {
+			int old_fd = conn_pool_get_old(pool, now - timeout + 1, &next);
+			if (old_fd >= 0) {
+				close(old_fd);
+			} else {
+				break;
+			}
+		}
+
+		if (next == 0) {
+			sleep(timeout);
+		} else {
+			sleep(next + timeout - now);
+		}
+	}
+
+	return NULL; // we never get here since the thread will be cancelled instead
+}
+
+conn_pool_t *conn_pool_init(size_t capacity, knot_timediff_t timeout)
+{
+	if (capacity == 0 || timeout == 0) {
+		return NULL;
+	}
+
+	conn_pool_t *pool = calloc(1, sizeof(*pool) + capacity * sizeof(pool->conns[0]));
+	if (pool != NULL) {
+		pool->capacity = capacity;
+		pool->timeout = timeout;
+		if (pthread_mutex_init(&pool->mutex, 0) != 0) {
+			free(pool);
+			return NULL;
+		}
+		if (pthread_create(&pool->closing_thread, NULL, closing_thread, pool) != 0) {
+			pthread_mutex_destroy(&pool->mutex);
+			free(pool);
+			return NULL;
+		}
+	}
+	return pool;
+}
+
+void conn_pool_deinit(conn_pool_t *pool)
+{
+	if (pool != NULL) {
+		pthread_cancel(pool->closing_thread);
+
+		int fd;
+		knot_time_t unused;
+		while ((fd = conn_pool_get_old(pool, 0, &unused)) >= 0) {
+			close(fd);
+		}
+
+		pthread_mutex_destroy(&pool->mutex);
+		free(pool);
+	}
+}
+
+knot_timediff_t conn_pool_timeout(conn_pool_t *pool,
+                                  knot_timediff_t new_timeout)
+{
+	if (pool == NULL) {
+		return 0;
+	}
+
+	pthread_mutex_lock(&pool->mutex);
+
+	knot_timediff_t prev = pool->timeout;
+	if (new_timeout != 0) {
+		pool->timeout = new_timeout;
+	}
+
+	pthread_mutex_unlock(&pool->mutex);
+	return prev;
+}
+
+static int pool_pop(conn_pool_t *pool, size_t i)
+{
+	conn_pool_memb_t *conn = &pool->conns[i];
+	assert(conn->last_active != 0);
+	assert(pool->usage > 0);
+	int fd = conn->fd;
+	memset(conn, 0, sizeof(*conn));
+	pool->usage--;
+	return fd;
+}
+
+int conn_pool_get(conn_pool_t *pool,
+                  struct sockaddr_storage *src,
+                  struct sockaddr_storage *dst)
+{
+	if (pool == NULL) {
+		return -1;
+	}
+
+	int fd = -1;
+	pthread_mutex_lock(&pool->mutex);
+
+	for (size_t i = 0; i < pool->capacity; i++) {
+		if (pool->conns[i].last_active != 0 &&
+		    sockaddr_cmp(&pool->conns[i].dst, dst, false) == 0 &&
+		    sockaddr_cmp(&pool->conns[i].src, src, false) == 0) {
+			fd = pool_pop(pool, i);
+			break;
+		}
+	}
+
+	pthread_mutex_unlock(&pool->mutex);
+
+	if (fd >= 0) {
+		uint8_t unused;
+		int peek = recv(fd, &unused, 1, MSG_PEEK | MSG_DONTWAIT); // returns 0 if fd closed by other side; -1 if still open
+		if (peek == 0) {
+			close(fd);
+			fd = -1;
+		}
+	}
+
+	return fd;
+}
+
+int conn_pool_get_old(conn_pool_t *pool,
+                      knot_time_t older_than,
+                      knot_time_t *next_oldest)
+{
+	*next_oldest = 0;
+	if (pool == NULL) {
+		return -1;
+	}
+
+	int fd = -1;
+	pthread_mutex_lock(&pool->mutex);
+
+	for (size_t i = 0; i < pool->capacity; i++) {
+		knot_time_t la = pool->conns[i].last_active;
+		if (fd == -1 && knot_time_cmp(la, older_than) < 0) {
+			fd = pool_pop(pool, i);
+		} else if (knot_time_cmp(la, *next_oldest) < 0) {
+			*next_oldest = la;
+		}
+	}
+
+	pthread_mutex_unlock(&pool->mutex);
+	return fd;
+}
+
+static void pool_push(conn_pool_t *pool, size_t i,
+                      struct sockaddr_storage *src,
+                      struct sockaddr_storage *dst,
+                      int fd)
+{
+	conn_pool_memb_t *conn = &pool->conns[i];
+	assert(conn->last_active == 0);
+	assert(pool->usage < pool->capacity);
+	conn->last_active = knot_time();
+	conn->fd = fd;
+	memcpy(&conn->src, src, sizeof(conn->src));
+	memcpy(&conn->dst, dst, sizeof(conn->dst));
+	pool->usage++;
+}
+
+bool conn_pool_put(conn_pool_t *pool,
+                   struct sockaddr_storage *src,
+                   struct sockaddr_storage *dst,
+                   int fd)
+{
+	if (pool == NULL) {
+		return false;
+	}
+
+	pthread_mutex_lock(&pool->mutex);
+
+	for (size_t i = 0; i < pool->capacity; i++) {
+		if (pool->conns[i].last_active == 0) {
+			pool_push(pool, i, src, dst, fd);
+			pthread_mutex_unlock(&pool->mutex);
+			return true;
+		}
+	}
+
+	pthread_mutex_unlock(&pool->mutex);
+	return false;
+}
+
+int conn_pool_put_force(conn_pool_t *pool,
+                        struct sockaddr_storage *src,
+                        struct sockaddr_storage *dst,
+                        int fd)
+{
+	if (pool == NULL || pool->capacity == 0) {
+		return fd;
+	}
+
+	knot_time_t oldest_time = 0;
+	size_t oldest_i = pool->capacity;
+
+	pthread_mutex_lock(&pool->mutex);
+
+	for (size_t i = 0; i < pool->capacity; i++) {
+		knot_time_t la = pool->conns[i].last_active;
+		if (la == 0) {
+			pool_push(pool, i, src, dst, fd);
+			pthread_mutex_unlock(&pool->mutex);
+			return -1;
+		} else if (knot_time_cmp(la, oldest_time) < 0) {
+			oldest_time = la;
+			oldest_i = i;
+		}
+	}
+
+	assert(oldest_i < pool->capacity);
+	int oldest_fd = pool_pop(pool, oldest_i);
+	pool_push(pool, oldest_i, src, dst, fd);
+	pthread_mutex_unlock(&pool->mutex);
+	return oldest_fd;
+}
diff --git a/src/contrib/conn_pool.h b/src/contrib/conn_pool.h
new file mode 100644
index 0000000000000000000000000000000000000000..4b2316fed4f16dd67e4484354b33090d77194b01
--- /dev/null
+++ b/src/contrib/conn_pool.h
@@ -0,0 +1,130 @@
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <pthread.h>
+#include <stdbool.h>
+#include <sys/socket.h>
+
+#include "contrib/time.h"
+
+typedef struct {
+	struct sockaddr_storage src;
+	struct sockaddr_storage dst;
+	int fd;
+	knot_time_t last_active;
+} conn_pool_memb_t;
+
+typedef struct {
+	size_t capacity;
+	size_t usage;
+	knot_timediff_t timeout;
+	pthread_mutex_t mutex;
+	pthread_t closing_thread;
+	conn_pool_memb_t conns[];
+} conn_pool_t;
+
+extern conn_pool_t *global_conn_pool;
+
+/*!
+ * \brief Allocate connection pool.
+ *
+ * \param capacity  Connection pool capacity (must be positive number).
+ * \param timeout   Connection timeout (must be positive number).
+ *
+ * \return Connection pool or NULL if error.
+ */
+conn_pool_t *conn_pool_init(size_t capacity, knot_timediff_t timeout);
+
+/*!
+ * \brief Deallocate the pool, close all connections, terminate closing thread.
+ *
+ * \param pool  Connection pool.
+ */
+void conn_pool_deinit(conn_pool_t *pool);
+
+/*!
+ * \brief Get and/or set connection timeout.
+ *
+ * \param pool         Connection pool.
+ * \param new_timeout  Optional: set new timeout (if positive number).
+ *
+ * \return Previous value of timeout.
+ */
+knot_timediff_t conn_pool_timeout(conn_pool_t *pool,
+                                  knot_timediff_t new_timeout);
+
+/*!
+ * \brief Try to get an open connection if present, check if alive.
+ *
+ * \param pool   Pool to search in.
+ * \param src    Connection source address.
+ * \param dst    Connection destination address.
+ *
+ * \retval -1    If error (no such connection).
+ * \return >= 0  File descriptor of the connection.
+ */
+int conn_pool_get(conn_pool_t *pool,
+                  struct sockaddr_storage *src,
+                  struct sockaddr_storage *dst);
+
+/*!
+ * \brief Try to get an open connection older than specified timestamp.
+ *
+ * \param pool           Pool to search in.
+ * \param older_than     Timestamp that the connection must be older than.
+ * \param next_oldest    Out: the timestamp of the oldest connection (other than the returned).
+ *
+ * \return -1 if error (no such connection), >= 0 connection file descriptor.
+ *
+ * \warning The returned connection is not necessarily the oldest one.
+ */
+int conn_pool_get_old(conn_pool_t *pool,
+                      knot_time_t older_than,
+                      knot_time_t *next_oldest);
+
+/*!
+ * \brief Put an open connection to the pool.
+ *
+ * \param pool   Pool to insert into.
+ * \param src    Connestion source address.
+ * \param dst    Connection destination adress.
+ * \param fd     Connection file descriptor.
+ *
+ * \return True if connection stored, false if not.
+ */
+bool conn_pool_put(conn_pool_t *pool,
+                   struct sockaddr_storage *src,
+                   struct sockaddr_storage *dst,
+                   int fd);
+
+/*!
+ * \brief Put an open connection to the pool, possibly displacing the oldest one there.
+ *
+ * \param pool   Pool to insert into.
+ * \param src    Connestion source address.
+ * \param dst    Connection destination adress.
+ * \param fd     Connection file descriptor.
+ *
+ * \retval -1    If connection stored to free slot.
+ * \retval fd    If not able to store connection.
+ * \return >= 0  File descriptor of the displaced old connection.
+ */
+int conn_pool_put_force(conn_pool_t *pool,
+                        struct sockaddr_storage *src,
+                        struct sockaddr_storage *dst,
+                        int fd);
diff --git a/src/knot/conf/schema.c b/src/knot/conf/schema.c
index 1b42449aa97577bda7b0996d9c3bd360e305995f..4cb321a9cf7256729dab8f7f37442c28f96eb023 100644
--- a/src/knot/conf/schema.c
+++ b/src/knot/conf/schema.c
@@ -195,6 +195,8 @@ static const yp_item_t desc_server[] = {
 	{ C_TCP_MAX_CLIENTS,      YP_TINT,  YP_VINT = { 0, INT32_MAX, YP_NIL } },
 	{ C_TCP_REUSEPORT,        YP_TBOOL, YP_VNONE },
 	{ C_TCP_FASTOPEN,         YP_TBOOL, YP_VNONE },
+	{ C_RMT_POOL_LIMIT,       YP_TINT,  YP_VINT = { 0, INT32_MAX, 0 } },
+	{ C_RMT_POOL_TIMEOUT,     YP_TINT,  YP_VINT = { 1, INT32_MAX, 5, YP_STIME } },
 	{ C_SOCKET_AFFINITY,      YP_TBOOL, YP_VNONE },
 	{ C_UDP_MAX_PAYLOAD,      YP_TINT,  YP_VINT = { KNOT_EDNS_MIN_DNSSEC_PAYLOAD,
 	                                                KNOT_EDNS_MAX_UDP_PAYLOAD,
diff --git a/src/knot/conf/schema.h b/src/knot/conf/schema.h
index d3806d72375c4d41346d926602d4f85bc4deb18e..7e12b8d44d0a62aa349fceb505c1a1144a98e2c8 100644
--- a/src/knot/conf/schema.h
+++ b/src/knot/conf/schema.h
@@ -95,6 +95,8 @@
 #define C_REFRESH_MIN_INTERVAL	"\x14""refresh-min-interval"
 #define C_REPRO_SIGNING		"\x14""reproducible-signing"
 #define C_RMT			"\x06""remote"
+#define C_RMT_POOL_LIMIT	"\x11""remote-pool-limit"
+#define C_RMT_POOL_TIMEOUT	"\x13""remote-pool-timeout"
 #define C_ROUTE_CHECK		"\x0B""route-check"
 #define C_RRSIG_LIFETIME	"\x0E""rrsig-lifetime"
 #define C_RRSIG_PREREFRESH	"\x11""rrsig-pre-refresh"
diff --git a/src/knot/query/requestor.c b/src/knot/query/requestor.c
index 59d7dcac91e68525a838ae76af81257e7279b158..77ab1be948b66e18327d69afbd627dc878a43190 100644
--- a/src/knot/query/requestor.c
+++ b/src/knot/query/requestor.c
@@ -19,6 +19,7 @@
 #include "libknot/attribute.h"
 #include "knot/query/requestor.h"
 #include "libknot/errcode.h"
+#include "contrib/conn_pool.h"
 #include "contrib/mempattern.h"
 #include "contrib/net.h"
 #include "contrib/sockaddr.h"
@@ -41,6 +42,16 @@ static int request_ensure_connected(knot_request_t *request)
 	}
 
 	int sock_type = use_tcp(request) ? SOCK_STREAM : SOCK_DGRAM;
+
+	if (sock_type == SOCK_STREAM) {
+		request->fd = conn_pool_get(global_conn_pool,
+		                            &request->source,
+		                            &request->remote);
+		if (request->fd >= 0) {
+			return KNOT_EOK;
+		}
+	}
+
 	request->fd = net_connected_socket(sock_type,
 	                                   &request->remote,
 	                                   &request->source,
@@ -156,6 +167,12 @@ void knot_request_free(knot_request_t *request, knot_mm_t *mm)
 		return;
 	}
 
+	if (request->fd >= 0 && use_tcp(request)) {
+		request->fd = conn_pool_put_force(global_conn_pool,
+		                                  &request->source,
+		                                  &request->remote,
+		                                  request->fd);
+	}
 	if (request->fd >= 0) {
 		close(request->fd);
 	}
diff --git a/src/knot/server/server.c b/src/knot/server/server.c
index d88d241a1f7a188318fd64605e2dbabc256085f6..e9b42d88b6fe57351a6f570653b579a5233b6cb8 100644
--- a/src/knot/server/server.c
+++ b/src/knot/server/server.c
@@ -38,6 +38,7 @@
 #include "knot/zone/timers.h"
 #include "knot/zone/zonedb-load.h"
 #include "knot/worker/pool.h"
+#include "contrib/conn_pool.h"
 #include "contrib/net.h"
 #include "contrib/openbsd/strlcat.h"
 #include "contrib/os.h"
@@ -696,6 +697,10 @@ void server_deinit(server_t *server)
 
 	/* Close journal database if open. */
 	knot_lmdb_deinit(&server->journaldb);
+
+	/* Close and deinit connection pool. */
+	conn_pool_deinit(global_conn_pool);
+	global_conn_pool = NULL;
 }
 
 static int server_init_handler(server_t *server, int index, int thread_count,
@@ -1190,6 +1195,18 @@ int server_reconfigure(conf_t *conf, server_t *server)
 		          knot_strerror(ret));
 	}
 
+	/* Reconfigure connection pool. */
+	conf_val_t val = conf_get(conf, C_SRV, C_RMT_POOL_LIMIT);
+	size_t cp_size = conf_int(&val);
+	val = conf_get(conf, C_SRV, C_RMT_POOL_TIMEOUT);
+	knot_timediff_t cp_timeout = conf_int(&val);
+	if (global_conn_pool == NULL && cp_size > 0) {
+		conn_pool_t *new_pool = conn_pool_init(cp_size, cp_timeout);
+		global_conn_pool = new_pool;
+	} else {
+		conn_pool_timeout(global_conn_pool, cp_timeout);
+	}
+
 	return KNOT_EOK;
 }
 
diff --git a/tests-extra/tools/dnstest/server.py b/tests-extra/tools/dnstest/server.py
index 90c0dda2edea6fa4311dfa934fb160436622ff56..68d967511404a849ef58febc45dbb49b9072ed69 100644
--- a/tests-extra/tools/dnstest/server.py
+++ b/tests-extra/tools/dnstest/server.py
@@ -1241,6 +1241,7 @@ class Knot(Server):
         self._str(s, "udp-max-payload", self.udp_max_payload)
         self._str(s, "udp-max-payload-ipv4", self.udp_max_payload_ipv4)
         self._str(s, "udp-max-payload-ipv6", self.udp_max_payload_ipv6)
+        self._str(s, "remote-pool-limit", str(random.randint(0,6)))
         s.end()
 
         s.begin("control")