diff --git a/daemon/bindings.c b/daemon/bindings.c
index b009814f84bf9b91c9344da498309d410643cbba..dc677b3e4421786ebf9b11e17d4973043f568c40 100644
--- a/daemon/bindings.c
+++ b/daemon/bindings.c
@@ -19,6 +19,23 @@
 #include "lib/cache.h"
 #include "daemon/bindings.h"
 
+/** @internal Prefix error with file:line */
+static int format_error(lua_State* L, const char *err)
+{
+	lua_Debug d;
+	lua_getstack(L, 1, &d);
+	/* error message prefix */
+	lua_getinfo(L, "Sln", &d);
+	lua_pushstring(L, d.short_src);
+	lua_pushstring(L, ":");
+	lua_pushnumber(L, d.currentline);
+	lua_pushstring(L, ": error: ");
+	/* error message */
+	lua_pushstring(L, err);
+	lua_concat(L,  5);
+	return 1;
+}
+
 /** @internal Compatibility wrapper for Lua 5.0 - 5.2 */
 #if LUA_VERSION_NUM >= 502
 #define register_lib(L, name, lib) \
@@ -49,7 +66,7 @@ static int mod_load(lua_State *L)
 	/* Check parameters */
 	int n = lua_gettop(L);
 	if (n != 1 || !lua_isstring(L, 1)) {
-		lua_pushstring(L, "expected module name");
+		lua_pushstring(L, "expected load(string name)");
 		lua_error(L);
 	}
 	/* Load engine module */
@@ -70,7 +87,7 @@ static int mod_unload(lua_State *L)
 	/* Check parameters */
 	int n = lua_gettop(L);
 	if (n != 1 || !lua_isstring(L, 1)) {
-		lua_pushstring(L, "expected module name");
+		format_error(L, "expected unload(string name)");
 		lua_error(L);
 	}
 	/* Unload engine module */
@@ -168,7 +185,7 @@ static int net_listen(lua_State *L)
 	if (lua_istable(L, 1)) {
 		return net_listen_iface(L, port);
 	} else if (n < 1 || !lua_isstring(L, 1)) {
-		lua_pushstring(L, "expected (string addr, int port = 53)");
+		format_error(L, "expected listen(string addr, int port = 53)");
 		lua_error(L);
 	}
 
@@ -190,7 +207,7 @@ static int net_close(lua_State *L)
 	/* Check parameters */
 	int n = lua_gettop(L);
 	if (n < 2) {
-		lua_pushstring(L, "expected (string addr, int port)");
+		format_error(L, "expected close(string addr, int port)");
 		lua_error(L);
 	}
 
@@ -291,8 +308,8 @@ static int cache_open(lua_State *L)
 {
 	/* Check parameters */
 	int n = lua_gettop(L);
-	if (n < 1) {
-		lua_pushstring(L, "expected (number max_size)");
+	if (n < 1 || !lua_isnumber(L, 1)) {
+		format_error(L, "expected open(number max_size)");
 		lua_error(L);
 	}
 
@@ -305,7 +322,7 @@ static int cache_open(lua_State *L)
 	/* Open resolution context cache */
 	engine->resolver.cache = kr_cache_open(".", engine->pool, lua_tointeger(L, 1));
 	if (engine->resolver.cache == NULL) {
-		lua_pushstring(L, "can't open cache in rundir");
+		format_error(L, "can't open cache in rundir");
 		lua_error(L);
 	}
 
diff --git a/doc/lib.rst b/doc/lib.rst
index c89712bd4548a74d7015d6154d9eb93b5ae7b7ba..5aec08f256609657313a6619d68e7e6fa1fe09fd 100644
--- a/doc/lib.rst
+++ b/doc/lib.rst
@@ -14,10 +14,9 @@ API reference
 Name resolution
 ---------------
 
-.. doxygengroup:: resolution
+.. doxygenfile:: resolve.h
    :project: libkresolve
-
-.. doxygengroup:: rplan
+.. doxygenfile:: rplan.h
    :project: libkresolve
 
 .. _lib_api_cache:
@@ -25,7 +24,7 @@ Name resolution
 Cache
 -----
 
-.. doxygengroup:: cache
+.. doxygenfile:: cache.h
    :project: libkresolve
 
 .. _lib_api_nameservers:
@@ -33,7 +32,9 @@ Cache
 Nameservers
 -----------
 
-.. doxygengroup:: nameservers
+.. doxygenfile:: nsrep.h
+   :project: libkresolve
+.. doxygenfile:: zonecut.h
    :project: libkresolve
 
 .. _lib_api_modules:
@@ -41,13 +42,15 @@ Nameservers
 Modules
 -------
 
-.. doxygengroup:: modules
+.. doxygenfile:: module.h
    :project: libkresolve
 
 Utilities
 ---------
 
-.. doxygengroup:: utils
+.. doxygenfile:: utils.h
+   :project: libkresolve
+.. doxygenfile:: defines.h
    :project: libkresolve
 
 .. _lib_generics:
diff --git a/lib/README.rst b/lib/README.rst
index 4b8bf299355b02ff450afdf0c12d1ffcbcc892bc..1e3fe89d28a58ba633f9be5bd561f808abffaadb 100644
--- a/lib/README.rst
+++ b/lib/README.rst
@@ -44,15 +44,15 @@ The library offers following services:
 - :ref:`Nameservers <lib_api_nameservers>` - Reputation database of nameservers, this serves as an aid for nameserver choice.
 
 A processing layer is going to be called by the query resolution driver for each query,
-so you're going to work with :ref:`struct kr_layer_param <lib_api_rplan>` as your per-query context. This structure contains pointers to
+so you're going to work with :ref:`struct kr_request <lib_api_rplan>` as your per-query context. This structure contains pointers to
 resolution context, resolution plan and also the final answer. You're likely to retrieve currently solved query from the query plan:
 
 .. code-block:: c
 
 	int consume(knot_layer_t *ctx, knot_pkt_t *pkt)
 	{
-		struct kr_layer_param *param = ctx->data;
-		struct kr_query *query = kr_rplan_current(param->rplan);
+		struct kr_request *request = ctx->data;
+		struct kr_query *query = kr_rplan_current(request->rplan);
 	}
 
 This is only passive processing of the incoming answer. If you want to change the course of resolution, say satisfy a query from a local cache before the library issues a query to the nameserver, you can use states (see the :ref:`Static hints <mod-hints>` for example).
@@ -61,8 +61,8 @@ This is only passive processing of the incoming answer. If you want to change th
 
 	int produce(knot_layer_t *ctx, knot_pkt_t *pkt)
 	{
-		struct kr_layer_param *param = ctx->data;
-		struct kr_query *cur = kr_rplan_current(param->rplan);
+		struct kr_request *request = ctx->data;
+		struct kr_query *cur = kr_rplan_current(request->rplan);
 		
 		/* Query can be satisfied locally. */
 		if (can_satisfy(cur)) {
@@ -83,8 +83,8 @@ This is useful for analysis-type tasks, or *"on-resolution"* hooks.
 
 	int finish(knot_layer_t *ctx)
 	{
-		struct kr_layer_param *param = ctx->data;
-		struct kr_rplan *rplan = param->rplan;
+		struct kr_request *request = ctx->data;
+		struct kr_rplan *rplan = request->rplan;
 
 		/* Print the query sequence with start time. */
 		char qname_str[KNOT_DNAME_MAXLEN];
diff --git a/lib/cache.h b/lib/cache.h
index 371a48f2e71ca6d20039d556081c18c87c137af1..ba658d78cd8efcca115ee4502d0e0a54aaefeea8 100644
--- a/lib/cache.h
+++ b/lib/cache.h
@@ -14,9 +14,6 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/** \addtogroup cache
- * @{
- */
 #pragma once
 
 #include <libknot/rrset.h>
@@ -117,5 +114,3 @@ int kr_cache_remove(namedb_txn_t *txn, const knot_rrset_t *rr);
  * @return KNOT_E*
  */
 int kr_cache_clear(namedb_txn_t *txn);
-
-/** @} */
diff --git a/lib/defines.h b/lib/defines.h
index cde6352f242839215de03c12e39de9949f2584ed..141f219557df72af09e1b7b5f38297ec84595925 100644
--- a/lib/defines.h
+++ b/lib/defines.h
@@ -14,10 +14,6 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/** \addtogroup utils
- * @{
- */
-
 #pragma once
 
 #include <errno.h>
@@ -36,7 +32,8 @@
  * Connection limits.
  * @cond internal
  */
-#define KR_CONN_RTT_MAX 5000
+#define KR_CONN_RTT_MAX 5000 /* Timeout for network activity */
+#define ITER_LIMIT 50        /* Built-in iterator limit */
 
 /*
  * Timers.
@@ -50,5 +47,3 @@
 #define KR_EDNS_VERSION 0
 #define KR_EDNS_PAYLOAD 4096
 /* @endcond */
-
-/** @} */
diff --git a/lib/generic/array.h b/lib/generic/array.h
index eb844fff94851b4dd0f41b137bc7c82313c65bb3..5655396f15a19110790b1e0ec6102acdd98790bd 100644
--- a/lib/generic/array.h
+++ b/lib/generic/array.h
@@ -23,14 +23,16 @@
  * Be aware of that, as direct usage of the macros in the evaluating macros
  * may lead to different expectations:
  *
- *     # Undefined behaviour
+ * @code{.c}
  *     MIN(array_push(arr, val), other)
+ * @endcode
  *
  * May evaluate the code twice, leading to unexpected behaviour.
  * This is a price to pay for the absence of proper generics.
  *
- * Example usage:
+ * # Example usage:
  *
+ * @code{.c}
  *      array_t(const char*) arr;
  *      array_init(arr);
  *
@@ -55,7 +57,7 @@
  *
  *      // Random delete
  *      array_del(arr, 0);
- *
+ * @endcode
  * \addtogroup generics
  * @{
  */
diff --git a/lib/generic/map.h b/lib/generic/map.h
index 7b04af51095de2b5d9675ba492ce6fae45b29dc3..7d40f9b528bb4f19718fc0a19c14a5d491fa3027 100644
--- a/lib/generic/map.h
+++ b/lib/generic/map.h
@@ -9,8 +9,9 @@
  *
  * @warning If the user provides a custom allocator, it must return addresses aligned to 2B boundary.
  *
- * Example usage:
+ * # Example usage:
  *
+ * @code{.c}
  *      map_t map = map_make();
  *
  *      // Custom allocator (optional)
@@ -43,6 +44,7 @@
  *
  *      // Clear the map
  *      map_clear(&map);
+ * @endcode
  *
  * \addtogroup generics
  * @{
diff --git a/lib/generic/pack.h b/lib/generic/pack.h
index bd3a8a774444b53016c11cf24a813e1beb1e442b..1fcdc616ce74d4d6f21f8b09ec8e2fe9b7958e4d 100644
--- a/lib/generic/pack.h
+++ b/lib/generic/pack.h
@@ -24,8 +24,9 @@
  *
  * @note Maximum object size is 2^16 bytes, see  ::pack_objlen_t
  *
- *  Example usage:
+ * # Example usage:
  *
+ * @code{.c}
  *      pack_t pack;
  *      pack_init(pack);
  *
@@ -47,6 +48,7 @@
  *      pack_obj_del(pack, U8("jedi"), 4);
  *
  *      pack_clear(pack);
+ * @endcode
  *
  * \addtogroup generics
  * @{
diff --git a/lib/generic/set.h b/lib/generic/set.h
index 26a3d36a08056494fdf8f3553159738ac692e9f8..be5be1602a7b54656014208fb37a63c4bc17d45a 100644
--- a/lib/generic/set.h
+++ b/lib/generic/set.h
@@ -20,8 +20,9 @@
  *
  * @note The API is based on map.h, see it for more examples.
  *
- * Example usage:
+ * # Example usage:
  *
+ * @code{.c}
  *      set_t set = set_make();
  *
  *      // Insert keys
@@ -50,6 +51,7 @@
  *
  *      // Clear the set
  *      set_clear(&set);
+ * @endcode
  *
  * \addtogroup generics
  * @{
diff --git a/lib/layer.h b/lib/layer.h
index 2189581a8fe12748bcc6e7726dd538a7c9ebfda1..0f7f314abf072181ee0888a16a8105fcf3aa98de 100644
--- a/lib/layer.h
+++ b/lib/layer.h
@@ -16,28 +16,8 @@
 
 #pragma once
 
-/** \addtogroup rplan
- * @{
- */
-
-#include <libknot/processing/layer.h>
-#include <libknot/packet/pkt.h>
-
 #include "lib/defines.h"
-
-struct kr_context;
-struct kr_rplan;
-
-/**
- * Processing module parameters.
- *
- * @note These parameters are passed to each processing layer.
- */
-struct kr_layer_param {
-	struct kr_context *ctx;
-	struct kr_rplan *rplan;
-	knot_pkt_t *answer;
-};
+#include "lib/resolve.h"
 
 #ifndef NDEBUG
 /** @internal Print a debug message related to resolution. */
@@ -48,6 +28,4 @@ struct kr_layer_param {
     } while (0)
 #else
  #define QRDEBUG(query, cls, fmt, ...)
-#endif
-
-/** @} */
+#endif
\ No newline at end of file
diff --git a/lib/layer/iterate.c b/lib/layer/iterate.c
index 3deac9c7507073e1ebd31ef49a257ce517d021f1..f7673cce31fe47cbdf2cf6a4da5434acc56d74d9 100644
--- a/lib/layer/iterate.c
+++ b/lib/layer/iterate.c
@@ -28,7 +28,7 @@
 #include "lib/nsrep.h"
 #include "lib/module.h"
 
-#define DEBUG_MSG(fmt...) QRDEBUG(kr_rplan_current(param->rplan), "iter", fmt)
+#define DEBUG_MSG(fmt...) QRDEBUG(kr_rplan_current(&req->rplan), "iter", fmt)
 
 /* Packet classification. */
 enum {
@@ -39,7 +39,7 @@ enum {
 };
 
 /* Iterator often walks through packet section, this is an abstraction. */
-typedef int (*rr_callback_t)(const knot_rrset_t *, unsigned, struct kr_layer_param *);
+typedef int (*rr_callback_t)(const knot_rrset_t *, unsigned, struct kr_request *);
 
 /** Return minimized QNAME/QTYPE for current zone cut. */
 static const knot_dname_t *minimized_qname(struct kr_query *query, uint16_t *qtype)
@@ -137,20 +137,20 @@ static int update_nsaddr(const knot_rrset_t *rr, struct kr_query *query, uint16_
 	return KNOT_STATE_CONSUME;
 }
 
-static int update_glue(const knot_rrset_t *rr, unsigned hint, struct kr_layer_param *param)
+static int update_glue(const knot_rrset_t *rr, unsigned hint, struct kr_request *req)
 {
-	return update_nsaddr(rr, kr_rplan_current(param->rplan), hint);
+	return update_nsaddr(rr, kr_rplan_current(&req->rplan), hint);
 }
 
-int rr_update_parent(const knot_rrset_t *rr, unsigned hint, struct kr_layer_param *param)
+int rr_update_parent(const knot_rrset_t *rr, unsigned hint, struct kr_request *req)
 {
-	struct kr_query *query = kr_rplan_current(param->rplan);
-	return update_nsaddr(rr, query->parent, hint);
+	struct kr_query *qry = kr_rplan_current(&req->rplan);
+	return update_nsaddr(rr, qry->parent, hint);
 }
 
-int rr_update_answer(const knot_rrset_t *rr, unsigned hint, struct kr_layer_param *param)
+int rr_update_answer(const knot_rrset_t *rr, unsigned hint, struct kr_request *req)
 {
-	knot_pkt_t *answer = param->answer;
+	knot_pkt_t *answer = req->answer;
 
 	/* Write copied RR to the result packet. */
 	int ret = knot_pkt_put(answer, KNOT_COMPR_HINT_NONE, rr, hint);
@@ -166,23 +166,23 @@ int rr_update_answer(const knot_rrset_t *rr, unsigned hint, struct kr_layer_para
 }
 
 /** Attempt to find glue for given nameserver name (best effort). */
-static int fetch_glue(knot_pkt_t *pkt, const knot_dname_t *ns, struct kr_layer_param *param)
+static int fetch_glue(knot_pkt_t *pkt, const knot_dname_t *ns, struct kr_request *req)
 {
 	int result = 0;
 	const knot_pktsection_t *ar = knot_pkt_section(pkt, KNOT_ADDITIONAL);
 	for (unsigned i = 0; i < ar->count; ++i) {
 		const knot_rrset_t *rr = knot_pkt_rr(ar, i);
 		if (knot_dname_is_equal(ns, rr->owner)) {
-			(void) update_glue(rr, 0, param);
+			(void) update_glue(rr, 0, req);
 			result += 1;
 		}
 	}
 	return result;
 }
 
-static int update_cut(knot_pkt_t *pkt, const knot_rrset_t *rr, struct kr_layer_param *param)
+static int update_cut(knot_pkt_t *pkt, const knot_rrset_t *rr, struct kr_request *req)
 {
-	struct kr_query *query = kr_rplan_current(param->rplan);
+	struct kr_query *query = kr_rplan_current(&req->rplan);	
 	struct kr_zonecut *cut = &query->zone_cut;
 	int state = KNOT_STATE_CONSUME;
 
@@ -203,7 +203,7 @@ static int update_cut(knot_pkt_t *pkt, const knot_rrset_t *rr, struct kr_layer_p
 	kr_zonecut_add(cut, knot_ns_name(&rr->rrs, 0), NULL);
 	for (unsigned i = 0; i < rr->rrs.rr_count; ++i) {
 		const knot_dname_t *ns_name = knot_ns_name(&rr->rrs, i);
-		int glue_records = fetch_glue(pkt, ns_name, param);
+		int glue_records = fetch_glue(pkt, ns_name, req);
 		/* Glue is mandatory for NS below zone */
 		if (knot_dname_in(ns_name, rr->owner) ) {
 			if (glue_records == 0) {
@@ -216,7 +216,7 @@ static int update_cut(knot_pkt_t *pkt, const knot_rrset_t *rr, struct kr_layer_p
 	return state;
 }
 
-static int process_authority(knot_pkt_t *pkt, struct kr_layer_param *param)
+static int process_authority(knot_pkt_t *pkt, struct kr_request *req)
 {
 	int result = KNOT_STATE_CONSUME;
 	const knot_pktsection_t *ns = knot_pkt_section(pkt, KNOT_AUTHORITY);
@@ -230,7 +230,7 @@ static int process_authority(knot_pkt_t *pkt, struct kr_layer_param *param)
 	for (unsigned i = 0; i < ns->count; ++i) {
 		const knot_rrset_t *rr = knot_pkt_rr(ns, i);
 		if (rr->type == KNOT_RRTYPE_NS) {
-			int state = update_cut(pkt, rr, param);
+			int state = update_cut(pkt, rr, req);
 			switch(state) {
 			case KNOT_STATE_DONE: result = state; break;
 			case KNOT_STATE_FAIL: return state; break;
@@ -244,10 +244,10 @@ static int process_authority(knot_pkt_t *pkt, struct kr_layer_param *param)
 	return result;
 }
 
-static void finalize_answer(knot_pkt_t *pkt, struct kr_layer_param *param)
+static void finalize_answer(knot_pkt_t *pkt, struct kr_request *req)
 {
 	/* Finalize header */
-	knot_pkt_t *answer = param->answer;
+	knot_pkt_t *answer = req->answer;
 	knot_wire_set_rcode(answer->wire, knot_wire_get_rcode(pkt->wire));
 
 	/* Fill in SOA if negative response */
@@ -258,16 +258,16 @@ static void finalize_answer(knot_pkt_t *pkt, struct kr_layer_param *param)
 		for (unsigned i = 0; i < ns->count; ++i) {
 			const knot_rrset_t *rr = knot_pkt_rr(ns, i);
 			if (rr->type == KNOT_RRTYPE_SOA) {
-				rr_update_answer(rr, 0, param);
+				rr_update_answer(rr, 0, req);
 				break;
 			}
 		}
 	}
 }
 
-static int process_answer(knot_pkt_t *pkt, struct kr_layer_param *param)
+static int process_answer(knot_pkt_t *pkt, struct kr_request *req)
 {
-	struct kr_query *query = kr_rplan_current(param->rplan);
+	struct kr_query *query = kr_rplan_current(&req->rplan);
 
 	/* Response for minimized QNAME.
 	 * NODATA   => may be empty non-terminal, retry (found zone cut)
@@ -292,7 +292,7 @@ static int process_answer(knot_pkt_t *pkt, struct kr_layer_param *param)
 	const knot_dname_t *cname = query->sname;
 	for (unsigned i = 0; i < an->count; ++i) {
 		const knot_rrset_t *rr = knot_pkt_rr(an, i);
-		int state = is_final ?  rr_update_answer(rr, 0, param) : rr_update_parent(rr, 0, param);
+		int state = is_final ?  rr_update_answer(rr, 0, req) : rr_update_parent(rr, 0, req);
 		if (state == KNOT_STATE_FAIL) {
 			return state;
 		}
@@ -301,10 +301,10 @@ static int process_answer(knot_pkt_t *pkt, struct kr_layer_param *param)
 
 	/* Follow canonical name as next SNAME. */
 	if (cname != query->sname) {
-		(void) kr_rplan_push(param->rplan, query->parent, cname, query->sclass, query->stype);
+		(void) kr_rplan_push(&req->rplan, query->parent, cname, query->sclass, query->stype);
 	} else {
 		if (query->parent == NULL) {
-			finalize_answer(pkt, param);
+			finalize_answer(pkt, req);
 		}
 	}
 
@@ -314,7 +314,7 @@ static int process_answer(knot_pkt_t *pkt, struct kr_layer_param *param)
 }
 
 /** Error handling, RFC1034 5.3.3, 4d. */
-static int resolve_error(knot_pkt_t *pkt, struct kr_layer_param *param)
+static int resolve_error(knot_pkt_t *pkt, struct kr_request *req)
 {
 	return KNOT_STATE_FAIL;
 }
@@ -333,9 +333,10 @@ static int begin(knot_layer_t *ctx, void *module_param)
 static int prepare_query(knot_layer_t *ctx, knot_pkt_t *pkt)
 {
 	assert(pkt && ctx);
-	struct kr_layer_param *param = ctx->data;
-	struct kr_query *query = kr_rplan_current(param->rplan);
+	struct kr_request *req = ctx->data;
+	struct kr_query *query = kr_rplan_current(&req->rplan);
 	if (query == NULL || ctx->state == KNOT_STATE_DONE) {
+		assert(0);
 		return ctx->state;
 	}
 
@@ -367,15 +368,6 @@ static int prepare_query(knot_layer_t *ctx, knot_pkt_t *pkt)
 		return KNOT_STATE_FAIL;
 	}
 
-#ifndef NDEBUG
-	char qname_str[KNOT_DNAME_MAXLEN], zonecut_str[KNOT_DNAME_MAXLEN], ns_str[SOCKADDR_STRLEN];
-	knot_dname_to_str(qname_str, qname, sizeof(qname_str));
-	struct sockaddr *addr = &query->ns.addr.ip;
-	inet_ntop(addr->sa_family, kr_nsrep_inaddr(query->ns.addr), ns_str, sizeof(ns_str));
-	knot_dname_to_str(zonecut_str, query->zone_cut.name, sizeof(zonecut_str));
-	DEBUG_MSG("=> querying: '%s' zone cut: '%s' m12n: '%s'\n", ns_str, zonecut_str, qname_str);
-#endif
-
 	/* Query built, expect answer. */
 	return KNOT_STATE_CONSUME;
 }
@@ -387,8 +379,8 @@ static int prepare_query(knot_layer_t *ctx, knot_pkt_t *pkt)
 static int resolve(knot_layer_t *ctx, knot_pkt_t *pkt)
 {
 	assert(pkt && ctx);
-	struct kr_layer_param *param = ctx->data;
-	struct kr_query *query = kr_rplan_current(param->rplan);
+	struct kr_request *req = ctx->data;
+	struct kr_query *query = kr_rplan_current(&req->rplan);
 	if (query == NULL || (query->flags & QUERY_RESOLVED)) {
 		return ctx->state;
 	}
@@ -396,20 +388,19 @@ static int resolve(knot_layer_t *ctx, knot_pkt_t *pkt)
 	/* Check for packet processing errors first. */
 	if (pkt->parsed < pkt->size) {
 		DEBUG_MSG("<= malformed response\n");
-		return resolve_error(pkt, param);
+		return resolve_error(pkt, req);
 	} else if (!is_paired_to_query(pkt, query)) {
 		DEBUG_MSG("<= ignoring mismatching response\n");
 		return KNOT_STATE_CONSUME;
 	} else if (knot_wire_get_tc(pkt->wire)) {
 		DEBUG_MSG("<= truncated response, failover to TCP\n");
-		struct kr_query *cur = kr_rplan_current(param->rplan);
-		if (cur) {
+		if (query) {
 			/* Fail if already on TCP. */
-			if (cur->flags & QUERY_TCP) {
+			if (query->flags & QUERY_TCP) {
 				DEBUG_MSG("<= TC=1 with TCP, bailing out\n");
-				return resolve_error(pkt, param);
+				return resolve_error(pkt, req);
 			}
-			cur->flags |= QUERY_TCP;
+			query->flags |= QUERY_TCP;
 		}
 		return KNOT_STATE_DONE;
 	}
@@ -424,16 +415,16 @@ static int resolve(knot_layer_t *ctx, knot_pkt_t *pkt)
 		break; /* OK */
 	default:
 		DEBUG_MSG("<= rcode: %s\n", rcode ? rcode->name : "??");
-		return resolve_error(pkt, param);
+		return resolve_error(pkt, req);
 	}
 
 	/* Resolve authority to see if it's referral or authoritative. */
 	int state = KNOT_STATE_CONSUME;
-	state = process_authority(pkt, param);
+	state = process_authority(pkt, req);
 	switch(state) {
 	case KNOT_STATE_CONSUME: /* Not referral, process answer. */
 		DEBUG_MSG("<= rcode: %s\n", rcode ? rcode->name : "??");
-		state = process_answer(pkt, param);
+		state = process_answer(pkt, req);
 		break;
 	case KNOT_STATE_DONE: /* Referral */
 		DEBUG_MSG("<= referral response, follow\n");
diff --git a/lib/layer/iterate.h b/lib/layer/iterate.h
index 3716e791161bc554a92b3bdf7fd38fff50fe0beb..db7de8bc1df5da3341042352de2ca569ff6092ef 100644
--- a/lib/layer/iterate.h
+++ b/lib/layer/iterate.h
@@ -26,13 +26,13 @@ extern const knot_layer_api_t *iterate_layer(void);
  * Result updates the query parent.
  * @note Hint is an index of chosen RR in the set.
  */
-int rr_update_parent(const knot_rrset_t *rr, unsigned hint, struct kr_layer_param *param);
+int rr_update_parent(const knot_rrset_t *rr, unsigned hint, struct kr_request *param);
 
 /**
  * Result updates the original query response.
  * @note When \a hint is KNOT_PF_FREE, RR is treated as a copy and answer takes its ownership.
  */
-int rr_update_answer(const knot_rrset_t *rr, unsigned hint, struct kr_layer_param *param);
+int rr_update_answer(const knot_rrset_t *rr, unsigned hint, struct kr_request *param);
 
 /* Processing module implementation. */
 const knot_layer_api_t *iterate_layer(void);
\ No newline at end of file
diff --git a/lib/layer/itercache.c b/lib/layer/itercache.c
index 3abd8180302d53183ac5c5d2c89503531d8eeb28..30ef1e06f585886474f04bd1f88e72d8a353c3ad 100644
--- a/lib/layer/itercache.c
+++ b/lib/layer/itercache.c
@@ -24,11 +24,11 @@
 #include "lib/cache.h"
 #include "lib/module.h"
 
-#define DEBUG_MSG(fmt...) QRDEBUG(kr_rplan_current(param->rplan), " cc ",  fmt)
+#define DEBUG_MSG(fmt...) QRDEBUG(kr_rplan_current(rplan), " cc ",  fmt)
 
-typedef int (*rr_callback_t)(const knot_rrset_t *, unsigned, struct kr_layer_param *);
+typedef int (*rr_callback_t)(const knot_rrset_t *, unsigned, struct kr_request *);
 
-static int update_parent(const knot_rrset_t *rr, unsigned drift, struct kr_layer_param *param)
+static int update_parent(const knot_rrset_t *rr, unsigned drift, struct kr_request *req)
 {
 	/* Find a first non-expired record. */
 	uint16_t i = 0;
@@ -39,12 +39,12 @@ static int update_parent(const knot_rrset_t *rr, unsigned drift, struct kr_layer
 		}
 	}
 
-	return rr_update_parent(rr, i, param);
+	return rr_update_parent(rr, i, req);
 }
 
-static int update_answer(const knot_rrset_t *rr, unsigned drift, struct kr_layer_param *param)
+static int update_answer(const knot_rrset_t *rr, unsigned drift, struct kr_request *req)
 {
-	knot_pkt_t *answer = param->answer;
+	knot_pkt_t *answer = req->answer;
 
 	/* Materialize RR set */
 	knot_rrset_t rr_copy = kr_cache_materialize(rr, drift, &answer->mm);
@@ -52,18 +52,18 @@ static int update_answer(const knot_rrset_t *rr, unsigned drift, struct kr_layer
 		return KNOT_STATE_FAIL;
 	}
 
-	return rr_update_answer(&rr_copy, 0, param);
+	return rr_update_answer(&rr_copy, 0, req);
 }
 
 static int read_cache_rr(namedb_txn_t *txn, knot_rrset_t *cache_rr, uint32_t timestamp,
-                         rr_callback_t cb, struct kr_layer_param *param)
+                         rr_callback_t cb, struct kr_request *req)
 {
 	/* Query cache for requested record */
 	if (kr_cache_peek(txn, cache_rr, &timestamp) != KNOT_EOK) {
 		return KNOT_STATE_NOOP;
 	}
 
-	return cb(cache_rr, timestamp, param);
+	return cb(cache_rr, timestamp, req);
 }
 
 static int begin(knot_layer_t *ctx, void *module_param)
@@ -75,13 +75,14 @@ static int begin(knot_layer_t *ctx, void *module_param)
 static int read_cache(knot_layer_t *ctx, knot_pkt_t *pkt)
 {
 	assert(pkt && ctx);
-	struct kr_layer_param *param = ctx->data;
-	struct kr_query *cur = kr_rplan_current(param->rplan);
+	struct kr_request *req = ctx->data;
+	struct kr_rplan *rplan = &req->rplan;
+	struct kr_query *cur = kr_rplan_current(rplan);
 	if (cur == NULL) {
 		return ctx->state;
 	}
 
-	namedb_txn_t *txn = kr_rplan_txn_acquire(param->rplan, NAMEDB_RDONLY);
+	namedb_txn_t *txn = kr_rplan_txn_acquire(rplan, NAMEDB_RDONLY);
 	uint32_t timestamp = cur->timestamp.tv_sec;
 	knot_rrset_t cache_rr;
 	knot_rrset_init(&cache_rr, cur->sname, cur->stype, cur->sclass);
@@ -93,7 +94,7 @@ static int read_cache(knot_layer_t *ctx, knot_pkt_t *pkt)
 	}
 
 	/* Try to find expected record first. */
-	int state = read_cache_rr(txn, &cache_rr, timestamp, callback, param);
+	int state = read_cache_rr(txn, &cache_rr, timestamp, callback, req);
 	if (state == KNOT_STATE_DONE) {
 		DEBUG_MSG("=> satisfied from cache\n");
 		cur->flags |= QUERY_RESOLVED;
@@ -102,11 +103,11 @@ static int read_cache(knot_layer_t *ctx, knot_pkt_t *pkt)
 
 	/* Check if CNAME chain exists. */
 	cache_rr.type = KNOT_RRTYPE_CNAME;
-	state = read_cache_rr(txn, &cache_rr, timestamp, callback, param);
+	state = read_cache_rr(txn, &cache_rr, timestamp, callback, req);
 	if (state != KNOT_STATE_NOOP) {
 		if (cur->stype != KNOT_RRTYPE_CNAME) {
 			const knot_dname_t *cname = knot_cname_name(&cache_rr.rrs);
-			if (kr_rplan_push(param->rplan, cur->parent, cname, cur->sclass, cur->stype) == NULL) {
+			if (kr_rplan_push(rplan, cur->parent, cname, cur->sclass, cur->stype) == NULL) {
 				return KNOT_STATE_FAIL;
 			}
 		}
@@ -229,8 +230,9 @@ static int write_cache_authority(knot_pkt_t *pkt, namedb_txn_t *txn, mm_ctx_t *p
 
 static int write_cache(knot_layer_t *ctx, knot_pkt_t *pkt)
 {
-	struct kr_layer_param *param = ctx->data;
-	struct kr_query *query = kr_rplan_current(param->rplan);
+	struct kr_request *req = ctx->data;
+	struct kr_rplan *rplan = &req->rplan;
+	struct kr_query *query = kr_rplan_current(rplan);
 
 	/* Don't cache anything if failed. */
 	if (query == NULL || ctx->state == KNOT_STATE_FAIL) {
@@ -238,9 +240,9 @@ static int write_cache(knot_layer_t *ctx, knot_pkt_t *pkt)
 	}
 
 	/* Open write transaction */
-	mm_ctx_t *pool = param->rplan->pool;
+	mm_ctx_t *pool = rplan->pool;
 	uint32_t timestamp = query->timestamp.tv_sec;
-	namedb_txn_t *txn = kr_rplan_txn_acquire(param->rplan, 0);
+	namedb_txn_t *txn = kr_rplan_txn_acquire(rplan, 0);
 	if (txn == NULL) {
 		return ctx->state; /* Couldn't acquire cache, ignore. */
 	}
diff --git a/lib/module.h b/lib/module.h
index bca4d3c626bab755a22bff2eef1d1106deda1afc..2b8c9c7eff5d97e6f02e20895db4b0a258491fc6 100644
--- a/lib/module.h
+++ b/lib/module.h
@@ -14,10 +14,6 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/** \addtogroup modules
- * @{
- */
-
 #pragma once
 
 #include <libknot/processing/layer.h>
@@ -92,5 +88,3 @@ void kr_module_unload(struct kr_module *module);
  */
 #define KR_MODULE_EXPORT(module) \
     uint32_t module ## _api() { return KR_MODULE_API; }
-
-/** @} */
diff --git a/lib/nsrep.c b/lib/nsrep.c
index 02f9bf9af5aa45b4dc58125662d93a6819a18552..9b8fb37ed3534196d8baa8d009ebfd97d7656d21 100644
--- a/lib/nsrep.c
+++ b/lib/nsrep.c
@@ -14,7 +14,10 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <assert.h>
+
 #include "lib/nsrep.h"
+#include "lib/defines.h"
 #include "lib/generic/pack.h"
 
 /** @internal Macro to set address structure. */
diff --git a/lib/nsrep.h b/lib/nsrep.h
index e61f063f2897545e13c5b834e315685b03fcc8a4..13cdef59ecb1fd9bdd051c2c1bcf21e542ab6bbf 100644
--- a/lib/nsrep.h
+++ b/lib/nsrep.h
@@ -14,31 +14,36 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/** \addtogroup nameservers
- * @{
- */
-
 #pragma once
 
 #include <netinet/in.h>
+#include <libknot/dname.h>
 
 #include "lib/generic/map.h"
-#include "lib/layer.h"
 
+/** 
+  * Special values for nameserver score.
+  * All positive values mean valid nameserver.
+  */
 enum kr_ns_score {
 	KR_NS_INVALID = 0,
 	KR_NS_VALID   = 1
 };
 
+/**
+ * Name server representation.
+ * Contains extra information about the name server, e.g. score
+ * or other metadata.
+ */
 struct kr_nsrep
 {
-	unsigned score;
-	const knot_dname_t *name;
+	unsigned score;                  /**< Server score */
+	const knot_dname_t *name;        /**< Server name */
 	union {
 		struct sockaddr ip;
 		struct sockaddr_in ip4;
 		struct sockaddr_in6 ip6;
-	} addr;
+	} addr;                          /**< Server address */
 };
 
 /** @internal Address bytes for given family. */
@@ -52,8 +57,6 @@ struct kr_nsrep
  * Elect best nameserver/address pair from the nsset.
  * @param  ns    updated NS representation
  * @param  nsset NS set to choose from
- * @return       0 if success (ns is updated), error otherwise
+ * @return       score, see enum kr_ns_score
  */
 int kr_nsrep_elect(struct kr_nsrep *ns, map_t *nsset);
-
-/** @} */
diff --git a/lib/resolve.c b/lib/resolve.c
index 014a5a180edef6a54f74d0d4289f114788bb633c..85650c2753a68514267692c612b057da35b74914 100644
--- a/lib/resolve.c
+++ b/lib/resolve.c
@@ -15,11 +15,13 @@
  */
 
 #include <stdio.h>
+#include <sys/fcntl.h>
 
 #include <libknot/internal/mempool.h>
 #include <libknot/processing/requestor.h>
 #include <libknot/rrtype/rdname.h>
 #include <libknot/descriptor.h>
+#include <libknot/internal/net.h>
 #include <dnssec/random.h>
 
 #include "lib/rplan.h"
@@ -27,10 +29,7 @@
 #include "lib/layer/itercache.h"
 #include "lib/layer/iterate.h"
 
-#define DEBUG_MSG(fmt...) QRDEBUG(kr_rplan_current(param->rplan), "resl",  fmt)
-
-/* Defines */
-#define ITER_LIMIT 50
+#define DEBUG_MSG(fmt...) QRDEBUG(kr_rplan_current(rplan), "resl",  fmt)
 
 /** Invalidate current NS/addr pair. */
 static int invalidate_ns(struct kr_rplan *rplan, struct kr_query *qry)
@@ -42,160 +41,314 @@ static int invalidate_ns(struct kr_rplan *rplan, struct kr_query *qry)
 	return kr_zonecut_del(&qry->zone_cut, qry->ns.name, rdata);
 }
 
-static int ns_resolve_addr(struct kr_query *cur, struct kr_layer_param *param)
+static int ns_resolve_addr(struct kr_query *qry, struct kr_request *param)
 {
-	if (kr_rplan_satisfies(cur, cur->ns.name, KNOT_CLASS_IN, KNOT_RRTYPE_A) ||
-	    kr_rplan_satisfies(cur, cur->ns.name, KNOT_CLASS_IN, KNOT_RRTYPE_AAAA) ||
-	    cur->flags & QUERY_AWAIT_ADDR) {
+	struct kr_rplan *rplan = &param->rplan;
+	if (kr_rplan_satisfies(qry, qry->ns.name, KNOT_CLASS_IN, KNOT_RRTYPE_A) ||
+	    kr_rplan_satisfies(qry, qry->ns.name, KNOT_CLASS_IN, KNOT_RRTYPE_AAAA) ||
+	    qry->flags & QUERY_AWAIT_ADDR) {
 		DEBUG_MSG("=> dependency loop, bailing out\n");
-		kr_rplan_pop(param->rplan, cur);
-		return KNOT_EOK;
+		kr_rplan_pop(rplan, qry);
+		return KNOT_STATE_PRODUCE;
+	}
+
+	(void) kr_rplan_push(rplan, qry, qry->ns.name, KNOT_CLASS_IN, KNOT_RRTYPE_AAAA);
+	(void) kr_rplan_push(rplan, qry, qry->ns.name, KNOT_CLASS_IN, KNOT_RRTYPE_A);
+	qry->flags |= QUERY_AWAIT_ADDR;
+	return KNOT_STATE_PRODUCE;
+}
+
+static void prepare_layers(struct kr_request *param)
+{
+	struct kr_context *ctx = param->ctx;
+	for (size_t i = 0; i < ctx->modules->len; ++i) {
+		struct kr_module *mod = &ctx->modules->at[i];
+		if (mod->layer) {
+			knot_overlay_add(&param->overlay, mod->layer(), param);
+		}
+	}
+}
+
+static int connected(struct sockaddr *addr, int proto, struct timeval *timeout)
+{
+	unsigned flags = (proto == SOCK_STREAM) ? O_NONBLOCK : 0;
+	int fd = net_connected_socket(proto, (struct sockaddr_storage *)addr, NULL, flags);
+	if (fd < 0) {
+		return kr_error(ECONNREFUSED);
 	}
 
-	(void) kr_rplan_push(param->rplan, cur, cur->ns.name, KNOT_CLASS_IN, KNOT_RRTYPE_AAAA);
-	(void) kr_rplan_push(param->rplan, cur, cur->ns.name, KNOT_CLASS_IN, KNOT_RRTYPE_A);
-	cur->flags |= QUERY_AWAIT_ADDR;
-	return KNOT_EOK;
+	/* Workaround for timeout, as we have no control over
+	 * connect() time limit in blocking mode. */
+	if (proto == SOCK_STREAM) {
+		fd_set set;
+		FD_ZERO(&set);
+		FD_SET(fd, &set);
+		int ret = select(fd + 1, NULL, &set, NULL, timeout);
+		if (ret == 0) {
+			close(fd);
+			return kr_error(ETIMEDOUT);
+		}
+		if (ret < 0) {
+			close(fd);
+			return kr_error(ECONNREFUSED);
+		}
+		fcntl(fd, F_SETFL, 0);
+	}
+
+	return fd;
 }
 
-static int iterate(struct knot_requestor *requestor, struct kr_layer_param *param)
+static int sendrecv(struct sockaddr *addr, int proto, const knot_pkt_t *query, knot_pkt_t *resp)
 {
-	int ret = KNOT_EOK;
 	struct timeval timeout = { KR_CONN_RTT_MAX / 1000, 0 };
-	struct kr_rplan *rplan = param->rplan;
-	struct kr_query *cur = kr_rplan_current(rplan);
+	auto_close int fd = connected(addr, proto, &timeout);
+	if (fd < 0) {
+		return fd;
+	}
 
-#ifndef NDEBUG
-	char name_str[KNOT_DNAME_MAXLEN], type_str[16];
-	knot_dname_to_str(name_str, cur->sname, sizeof(name_str));
-	knot_rrtype_to_string(cur->stype, type_str, sizeof(type_str));
-	DEBUG_MSG("query '%s %s'\n", name_str, type_str);
-#endif
+	/* Send packet */
+	int ret = 0;
+	if (proto == SOCK_STREAM) {
+		ret = tcp_send_msg(fd, query->wire, query->size, &timeout);
+	} else {
+		ret = udp_send_msg(fd, query->wire, query->size, NULL);
+	}
+	if (ret != query->size) {
+		return kr_error(EIO);
+	}
 
-	/* Elect best nameserver candidate. */
-	kr_nsrep_elect(&cur->ns, &cur->zone_cut.nsset);
-	if (cur->ns.score < KR_NS_VALID) {
-		DEBUG_MSG("=> no valid NS left\n");
-		kr_rplan_pop(param->rplan, cur);
-		return KNOT_EOK;
+	/* Receive it */
+	if (proto == SOCK_STREAM) {
+		ret = tcp_recv_msg(fd, resp->wire, resp->max_size, &timeout);
 	} else {
-		if (cur->ns.addr.ip.sa_family == AF_UNSPEC) {
-			DEBUG_MSG("=> ns missing A/AAAA, fetching\n");
-			return ns_resolve_addr(cur, param);
-		}
+		ret = udp_recv_msg(fd, resp->wire, resp->max_size, &timeout);
+	}
+	if (ret <= 0) {
+		return kr_error(ETIMEDOUT);
 	}
 
-	/* Prepare query resolution. */
-	int mode = (cur->flags & QUERY_TCP) ? 0 : KNOT_RQ_UDP;
-	knot_pkt_t *query = knot_pkt_new(NULL, KNOT_WIRE_MIN_PKTSIZE, requestor->mm);
-	struct knot_request *tx = knot_request_make(requestor->mm, &cur->ns.addr.ip, NULL, query, mode);
-	knot_requestor_enqueue(requestor, tx);
+	/* Parse and return */
+	resp->size = ret;
+	if (knot_pkt_parse(resp, 0) != 0) {
+		return kr_error(EBADMSG);
+	}
 
-	/* Resolve and check status. */
-	ret = knot_requestor_exec(requestor, &timeout);
-	if (ret != KNOT_EOK) {
-		/* Network error, retry over TCP. */
-		if (ret != KNOT_LAYER_ERROR && !(cur->flags & QUERY_TCP)) {
-			DEBUG_MSG("=> ns unreachable, retrying over TCP\n");
-			cur->flags |= QUERY_TCP;
-			return iterate(requestor, param);
+	return kr_ok();
+}
+
+int kr_resolve(struct kr_context* ctx, knot_pkt_t *answer,
+               const knot_dname_t *qname, uint16_t qclass, uint16_t qtype)
+{
+	if (ctx == NULL || answer == NULL || qname == NULL) {
+		return kr_error(EINVAL);
+	}
+
+	/* Create memory pool */
+	mm_ctx_t pool;
+	mm_ctx_mempool(&pool, MM_DEFAULT_BLKSIZE);
+	knot_pkt_t *query = knot_pkt_new(NULL, KNOT_WIRE_MIN_PKTSIZE, &pool);
+	knot_pkt_t *resp = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, &pool);
+	if (!query || !resp) {
+		mp_delete(pool.ctx);
+		return kr_error(ENOMEM);
+	}
+
+	/* Initialize context. */
+	struct kr_request request;
+	request.pool = pool;
+	kr_resolve_begin(&request, ctx, answer);
+#ifndef NDEBUG
+	struct kr_rplan *rplan = &request.rplan; /* for DEBUG_MSG */
+#endif
+
+	/* Resolve query, iteratively */
+	int proto = 0;
+	struct sockaddr *addr = NULL;
+	unsigned iter_count = 0;
+	int state = kr_resolve_query(&request, qname, qclass, qtype);
+	while (state == KNOT_STATE_PRODUCE) {
+		/* Hardlimit on iterative queries */
+		if (++iter_count > ITER_LIMIT) {
+			DEBUG_MSG("iteration limit %d reached\n", ITER_LIMIT);
+			state = KNOT_STATE_FAIL;
+			break;
 		}
-		/* Resolution failed, invalidate current NS and reset to UDP. */
-		DEBUG_MSG("=> resolution failed: '%s', invalidating\n", knot_strerror(ret));
-		if (invalidate_ns(rplan, cur) == 0) {
-			cur->flags &= ~QUERY_TCP;
+		/* Produce next query or finish */
+		state = kr_resolve_produce(&request, &addr, &proto, query);
+		while (state == KNOT_STATE_CONSUME) {
+			/* Get answer from nameserver and consume it */
+			int ret = sendrecv(addr, proto, query, resp);
+			if (ret != 0) {
+				DEBUG_MSG("sendrecv: %s\n", kr_strerror(ret));
+				resp->size = 0;
+			}
+			state = kr_resolve_consume(&request, resp);
+			knot_pkt_clear(resp);
 		}
-		return KNOT_EOK;
+		knot_pkt_clear(query);
 	}
 
-	/* Pop query if resolved. */
-	if (cur->flags & QUERY_RESOLVED) {
-		kr_rplan_pop(rplan, cur);
-	}
+	/* Cleanup */
+	kr_resolve_finish(&request, state);
+	mp_delete(pool.ctx);
+	return state == KNOT_STATE_DONE ? 0 : kr_error(EIO);
+}
+
+
+int kr_resolve_begin(struct kr_request *request, struct kr_context *ctx, knot_pkt_t *answer)
+{
+	/* Initialize request */
+	kr_rplan_init(&request->rplan, ctx, &request->pool);
+	knot_overlay_init(&request->overlay, &request->pool);
+	request->ctx = ctx;
+	request->answer = answer;
+	prepare_layers(request);
 
-	return ret;
+	/* Expect first query */
+	return KNOT_STATE_CONSUME;
 }
 
-static void prepare_layers(struct knot_requestor *req, struct kr_layer_param *param)
+int kr_resolve_query(struct kr_request *request, const knot_dname_t *qname, uint16_t qclass, uint16_t qtype)
 {
-	struct kr_context *ctx = param->ctx;
-	for (size_t i = 0; i < ctx->modules->len; ++i) {
-		struct kr_module *mod = &ctx->modules->at[i];
-		if (mod->layer) {
-			knot_requestor_overlay(req, mod->layer(), param);
-		}
+	struct kr_rplan *rplan = &request->rplan;
+	struct kr_query *qry = kr_rplan_push(rplan, NULL, qname, qclass, qtype);
+	if (!qry) {
+		return KNOT_STATE_FAIL;
 	}
+
+	/* Create answer packet */
+	knot_pkt_t *answer = request->answer;
+	knot_wire_set_qr(answer->wire);
+	knot_wire_clear_aa(answer->wire);
+	knot_wire_set_ra(answer->wire);
+	knot_wire_set_rcode(answer->wire, KNOT_RCODE_NOERROR);
+
+	/* Expect answer */
+	return KNOT_STATE_PRODUCE;
 }
 
-static int resolve_iterative(struct kr_layer_param *param, mm_ctx_t *pool)
+int kr_resolve_consume(struct kr_request *request, knot_pkt_t *packet)
 {
-/* Initialize requestor. */
-	struct knot_requestor requestor;
-	knot_requestor_init(&requestor, pool);
-	prepare_layers(&requestor, param);
+	struct kr_rplan *rplan = &request->rplan;
+	struct kr_query *qry = kr_rplan_current(rplan);
 
-	/* Iteratively solve the query. */
-	int ret = KNOT_EOK;
-	unsigned iter_count = 0;
-	while((ret == KNOT_EOK) && !kr_rplan_empty(param->rplan)) {
-		ret = iterate(&requestor, param);
-		if (++iter_count > ITER_LIMIT) {
-			DEBUG_MSG("iteration limit %d reached\n", ITER_LIMIT);
-			ret = KNOT_ELIMIT;
+	/* Empty resolution plan, push packet as the new query */
+	if (kr_rplan_empty(&request->rplan)) {
+		const knot_dname_t *qname = knot_pkt_qname(packet);
+		uint16_t qclass = knot_pkt_qclass(packet);
+		uint16_t qtype = knot_pkt_qtype(packet);
+		return kr_resolve_query(request, qname, qclass, qtype);
+	}
+
+	/* Different processing for network error */
+	int state = KNOT_STATE_FAIL;
+	if (!packet || packet->size == 0) {
+		/* Network error, retry over TCP. */
+		if (!(qry->flags & QUERY_TCP)) {
+			/** @todo This should just penalize UDP and elect next best. */
+			DEBUG_MSG("=> ns unreachable, retrying over TCP\n");
+			qry->flags |= QUERY_TCP;
+			return KNOT_STATE_CONSUME; /* Try again */
 		}
+	} else {
+		state = knot_overlay_consume(&request->overlay, packet);
 	}
 
-	/* Set RCODE on internal failure. */
-	if (ret != KNOT_EOK) {
-		if (knot_wire_get_rcode(param->answer->wire) == KNOT_RCODE_NOERROR) {
-			knot_wire_set_rcode(param->answer->wire, KNOT_RCODE_SERVFAIL);
+	/* Resolution failed, invalidate current NS and reset to UDP. */
+	if (state == KNOT_STATE_FAIL) {
+		DEBUG_MSG("=> resolution failed, invalidating\n");
+		if (invalidate_ns(rplan, qry) == 0) {
+			qry->flags &= ~QUERY_TCP;
 		}
 	}
 
-	DEBUG_MSG("finished: %s, mempool: %zu B\n", knot_strerror(ret), (size_t) mp_total_size(pool->ctx));
-	knot_requestor_clear(&requestor);
-	return ret;
+	/* Pop query if resolved. */
+	if (qry->flags & QUERY_RESOLVED) {
+		kr_rplan_pop(rplan, qry);
+
+	}
+
+	knot_overlay_reset(&request->overlay);
+	return kr_rplan_empty(&request->rplan) ? KNOT_STATE_DONE : KNOT_STATE_PRODUCE;
 }
 
-int kr_resolve(struct kr_context* ctx, knot_pkt_t *answer,
-               const knot_dname_t *qname, uint16_t qclass, uint16_t qtype)
+int kr_resolve_produce(struct kr_request *request, struct sockaddr **dst, int *type, knot_pkt_t *packet)
 {
-	if (ctx == NULL || answer == NULL || qname == NULL) {
-		return KNOT_EINVAL;
+	struct kr_rplan *rplan = &request->rplan;
+	struct kr_query *qry = kr_rplan_current(rplan);
+	
+	/* No query left for resolution */
+	if (kr_rplan_empty(rplan)) {
+		return KNOT_STATE_FAIL;
 	}
 
-	/* Initialize context. */
-	int ret = KNOT_EOK;
-	mm_ctx_t rplan_pool;
-	mm_ctx_mempool(&rplan_pool, MM_DEFAULT_BLKSIZE);
-	struct kr_rplan rplan;
-	kr_rplan_init(&rplan, ctx, &rplan_pool);
-	struct kr_layer_param param;
-	param.ctx = ctx;
-	param.rplan = &rplan;
-	param.answer = answer;
-
-	/* Push query to resolution plan. */
-	struct kr_query *qry = kr_rplan_push(&rplan, NULL, qname, qclass, qtype);
-	if (qry != NULL) {
-		ret = resolve_iterative(&param, &rplan_pool);
+#ifndef NDEBUG
+	char name_str[KNOT_DNAME_MAXLEN], type_str[16];
+	knot_dname_to_str(name_str, qry->sname, sizeof(name_str));
+	knot_rrtype_to_string(qry->stype, type_str, sizeof(type_str));
+	DEBUG_MSG("query '%s %s'\n", type_str, name_str);
+#endif
+
+	/* Resolve current query and produce dependent or finish */
+	int state = knot_overlay_produce(&request->overlay, packet);
+	switch(state) {
+	case KNOT_STATE_FAIL: return state; break;
+	case KNOT_STATE_CONSUME: break;
+	default: /* Current query is done */
+		knot_overlay_reset(&request->overlay);
+		kr_rplan_pop(rplan, qry);
+		return kr_rplan_empty(rplan) ? KNOT_STATE_DONE : KNOT_STATE_PRODUCE;
+	}
+
+	/* Elect best nameserver candidate */
+	kr_nsrep_elect(&qry->ns, &qry->zone_cut.nsset);
+	if (qry->ns.score < KR_NS_VALID) {
+		DEBUG_MSG("=> no valid NS left\n");
+		knot_overlay_reset(&request->overlay);
+		kr_rplan_pop(rplan, qry);
+		return KNOT_STATE_PRODUCE;
 	} else {
-		ret = KNOT_ENOMEM;
+		if (qry->ns.addr.ip.sa_family == AF_UNSPEC) {
+			DEBUG_MSG("=> ns missing A/AAAA, fetching\n");
+			knot_overlay_reset(&request->overlay);
+			return ns_resolve_addr(qry, request);
+		}
 	}
 
-	/* Check flags. */
-	knot_wire_set_qr(answer->wire);
-	knot_wire_clear_aa(answer->wire);
-	knot_wire_set_ra(answer->wire);
+#ifndef NDEBUG
+	char qname_str[KNOT_DNAME_MAXLEN], zonecut_str[KNOT_DNAME_MAXLEN], ns_str[SOCKADDR_STRLEN];
+	knot_dname_to_str(qname_str, knot_pkt_qname(packet), sizeof(qname_str));
+	struct sockaddr *addr = &qry->ns.addr.ip;
+	inet_ntop(addr->sa_family, kr_nsrep_inaddr(qry->ns.addr), ns_str, sizeof(ns_str));
+	knot_dname_to_str(zonecut_str, qry->zone_cut.name, sizeof(zonecut_str));
+	DEBUG_MSG("=> querying: '%s' zone cut: '%s' m12n: '%s'\n", ns_str, zonecut_str, qname_str);
+#endif
+
+	/* Issue dependent query to this address */
+	*dst = &qry->ns.addr.ip;
+	*type = (qry->flags & QUERY_TCP) ? SOCK_STREAM : SOCK_DGRAM;
+	return state;
+}
+
+int kr_resolve_finish(struct kr_request *request, int state)
+{
+	struct kr_rplan *rplan = &request->rplan;
+	DEBUG_MSG("finished: %d, mempool: %zu B\n", state, (size_t) mp_total_size(request->pool.ctx));
 
 	/* Resolution success, commit cache transaction. */
-	if (ret == KNOT_EOK) {
-		kr_rplan_txn_commit(&rplan);
+	if (state == KNOT_STATE_DONE) {
+		kr_rplan_txn_commit(rplan);
+	} else {
+		/* Error during procesing, internal failure */
+		knot_pkt_t *answer = request->answer;
+		if (knot_wire_get_rcode(answer->wire) == KNOT_RCODE_NOERROR) {
+			knot_wire_set_rcode(answer->wire, KNOT_RCODE_SERVFAIL);
+		}
 	}
 
 	/* Clean up. */
-	kr_rplan_deinit(&rplan);
-	mp_delete(rplan_pool.ctx);
-
-	return ret;
+	knot_overlay_reset(&request->overlay);
+	knot_overlay_deinit(&request->overlay);
+	kr_rplan_deinit(&request->rplan);
+	return KNOT_STATE_DONE;
 }
diff --git a/lib/resolve.h b/lib/resolve.h
index 7319c246ebd25ec67ff4872b0b5be7f48dc5ac99..ec8db797a4864a6316c70d4ddea1639ba89d0a63 100644
--- a/lib/resolve.h
+++ b/lib/resolve.h
@@ -14,19 +14,78 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/** \addtogroup resolution
- * @{
- */
-
 #pragma once
 
+#include <netinet/in.h>
+#include <libknot/processing/overlay.h>
 #include <libknot/packet/pkt.h>
 
 #include "lib/generic/array.h"
+#include "lib/rplan.h"
 #include "lib/module.h"
 
-/** Array of modules. */
+/**
+ * @file resolve.h
+ * @brief The API provides a high-level API for simple name resolution,
+ * and an API providing a "consumer-producer"-like interface to enable
+ * you write custom I/O or special iterative resolution driver.
+ *
+ * # Example usage of the high-level API:
+ *
+ * @code{.c}
+ *
+ * struct kr_context ctx = {
+ *     .pool = NULL, // for persistent data
+ *     .cache = ..., // open cache instance (or NULL)
+ *     .layers = {}  // loaded layers
+ * };
+ *
+ * // Push basic layers
+ * array_push(ctx.layers, iterate_layer);
+ * array_push(ctx.layers, itercache_layer);
+ *
+ * // Resolve "IN A cz."
+ * knot_pkt_t *answer = knot_pkt_new(NULL, 65535, ctx.pool);
+ * int ret = kr_resolve(&ctx, answer, (uint8_t*)"\x02cz", 1, 1);
+ * printf("rcode: %d, ancount: %u\n",
+ *        knot_wire_get_rcode(answer->wire),
+ *        knot_wire_get_ancount(answer->wire));
+ * @endcode
+ *
+ * # Example usage of the iterative API:
+ *
+ * @code{.c}
+ *
+ * // Create request and its memory pool
+ * struct kr_request req;
+ * mm_ctx_mempool(&req.pool, 4096);
+ * kr_resolve_begin(&req, ctx, answer);
+ * int state = kr_resolve_query(&req, qname, qclass, qtype);
+ *
+ * // Generate answer
+ * while (state == KNOT_STATE_PRODUCE) {
+ *
+ *     // Additional query generate, do the I/O and pass back answer
+ *     state = kr_resolve_produce(&req, &addr, &type, query);
+ *     while (state == KNOT_STATE_CONSUME) {
+ *         int ret = sendrecv(addr, proto, query, resp);
+ *
+ *         // If I/O fails, make "resp" empty
+ *         state = kr_resolve_consume(&request, resp);
+ *         knot_pkt_clear(resp);
+ *     }
+ *     knot_pkt_clear(query);
+ * }
+ *
+ * // "state" is either DONE or FAIL
+ * kr_resolve_finish(&request, state);
+ *
+ * @endcode
+ */
+
+/* @cond internal Array of modules. */
 typedef array_t(struct kr_module) module_array_t;
+/* @endcond */
 
 /**
  * Name resolution context.
@@ -44,17 +103,96 @@ struct kr_context
 	uint32_t options;
 };
 
+/**
+ * Name resolution request.
+ *
+ * Keeps information about current query processing between calls to
+ * processing APIs, i.e. current resolved query, resolution plan, ...
+ * Use this instead of the simple interface if you want to implement
+ * multiplexing or custom I/O.
+ *
+ * @note All data for this request must be allocated from the given pool.
+ */
+struct kr_request {
+    struct kr_context *ctx;
+    struct kr_rplan rplan;
+    struct knot_overlay overlay;
+    knot_pkt_t *answer;
+    mm_ctx_t pool;
+};
+
 /**
  * Resolve an input query and produce a packet with an answer.
+ *
  * @note The function doesn't change the packet question or message ID.
- * @param ctx resolution context
+ *
+ * @param ctx    resolution context
  * @param answer answer packet to be written
- * @param qname resolved query name
+ * @param qname  resolved query name
  * @param qclass resolved query class
- * @param qtype resolved query type
- * @return KNOT_E*
+ * @param qtype  resolved query type
+ * @return       0 or an error code
  */
 int kr_resolve(struct kr_context* ctx, knot_pkt_t *answer,
                const knot_dname_t *qname, uint16_t qclass, uint16_t qtype);
 
-/** @} */
+/**
+ * Begin name resolution.
+ *
+ * @note Expects a request to have an initialized mempool, the "answer" packet will
+ *       be kept during the resolution and will contain the final answer at the end.
+ *
+ * @param request request state with initialized mempool
+ * @param ctx     resolution context
+ * @param answer  allocated packet for final answer
+ * @return        CONSUME (expecting query)
+ */
+int kr_resolve_begin(struct kr_request *request, struct kr_context *ctx, knot_pkt_t *answer);
+
+/**
+ * Push new query for resolution to the state.
+ * @param  request request state (if already has a question, this will be resolved first)
+ * @param  qname
+ * @param  qclass
+ * @param  qtype
+ * @return         PRODUCE|FAIL
+ */
+int kr_resolve_query(struct kr_request *request, const knot_dname_t *qname, uint16_t qclass, uint16_t qtype);
+
+/**
+ * Consume input packet (may be either first query or answer to query originated from kr_resolve_produce())
+ *
+ * @note If the I/O fails, provide an empty or NULL packet, this will make iterator recognize nameserver failure.
+ * 
+ * @param  request request state (awaiting input)
+ * @param  packet  [in] input packet
+ * @return         any state
+ */
+int kr_resolve_consume(struct kr_request *request, knot_pkt_t *packet);
+
+/**
+ * Produce either next additional query or finish.
+ *
+ * If the CONSUME is returned then dst, type and packet will be filled with
+ * appropriate values and caller is responsible to send them and receive answer.
+ * If it returns any other state, then content of the variables is undefined.
+ * 
+ * @param  request request state (in PRODUCE state)
+ * @param  dst     [out] possible address of the next nameserver
+ * @param  type    [out] possible used socket type (SOCK_STREAM, SOCK_DGRAM)
+ * @param  packet  [out] packet to be filled with additional query
+ * @return         any state
+ */
+int kr_resolve_produce(struct kr_request *request, struct sockaddr **dst, int *type, knot_pkt_t *packet);
+
+/**
+ * Finish resolution and commit results if the state is DONE.
+ *
+ * @note The structures will be deinitialized, but the assigned memory pool is not going to
+ *       be destroyed, as it's owned by caller.
+ *
+ * @param  request request state
+ * @param  state   either DONE or FAIL state
+ * @return         DONE
+ */
+int kr_resolve_finish(struct kr_request *request, int state);
diff --git a/lib/rplan.h b/lib/rplan.h
index 534280d3c2204faaaa0aefa7b6b2e87ccdd5f5bd..07aa951fa1d74eca53268afc2643898e984dbd49 100644
--- a/lib/rplan.h
+++ b/lib/rplan.h
@@ -14,14 +14,9 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/** \addtogroup rplan
- * @{
- */
-
 #pragma once
 
 #include <sys/time.h>
-
 #include <libknot/dname.h>
 #include <libknot/internal/lists.h>
 #include <libknot/internal/namedb/namedb.h>
@@ -142,5 +137,3 @@ struct kr_query *kr_rplan_current(struct kr_rplan *rplan);
  * Return true if resolution chain satisfies given query.
  */
 bool kr_rplan_satisfies(struct kr_query *closure, const knot_dname_t *name, uint16_t cls, uint16_t type);
-
-/** @} */
diff --git a/lib/utils.h b/lib/utils.h
index 24f8c4af2ce0c4751655dcf321dc0e17e00a9292..219b662a770bf352909b7cdbd6eba94073409ab4 100644
--- a/lib/utils.h
+++ b/lib/utils.h
@@ -14,10 +14,6 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/** \addtogroup utils
- * @{
- */
-
 #pragma once
 
 #include <stdio.h>
@@ -40,5 +36,3 @@ extern void _cleanup_fclose(FILE **p);
 
 /** Concatenate N strings. */
 char* kr_strcatdup(unsigned n, ...);
-
-/** @} */
diff --git a/lib/zonecut.h b/lib/zonecut.h
index ed2cf3765a515d3bc55669702ef6698e5db0f7e3..fd708c1b0577fa6a577a0df4233945751c12b3ac 100644
--- a/lib/zonecut.h
+++ b/lib/zonecut.h
@@ -14,15 +14,11 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/** \addtogroup rplan
- * @{
- */
-
 #pragma once
 
-#include "lib/cache.h"
 #include "lib/generic/map.h"
 #include "lib/generic/pack.h"
+#include "lib/cache.h"
 
 struct kr_rplan;
 
@@ -31,8 +27,8 @@ struct kr_rplan;
 */
 struct kr_zonecut {
 	knot_dname_t *name; /**< Zone cut name. */
-    mm_ctx_t *pool;     /**< Memory pool. */
-    map_t nsset;        /**< Map of nameserver => address_set. */
+	mm_ctx_t *pool;     /**< Memory pool. */
+	map_t nsset;        /**< Map of nameserver => address_set. */
 };
 
 /**
@@ -109,5 +105,3 @@ int kr_zonecut_set_sbelt(struct kr_zonecut *cut);
  * @return 0 or error code
  */
 int kr_zonecut_find_cached(struct kr_zonecut *cut, namedb_txn_t *txn, uint32_t timestamp);
-
-/** @} */
diff --git a/modules/README.rst b/modules/README.rst
index bd753439097fa792c02ae45487fc49b2617a58ac..66072265488ea2717d359a4466d8256bdd9fb5a5 100644
--- a/modules/README.rst
+++ b/modules/README.rst
@@ -164,7 +164,7 @@ Now we can add the implementations for the ``Begin`` and ``Finish`` functions, a
 
 	func Finish(ctx *C.knot_layer_t) C.int {
 		// Since the context is unsafe.Pointer, we need to cast it
-		var param *C.struct_kr_layer_param = (*C.struct_kr_layer_param)(ctx.data)
+		var param *C.struct_kr_request = (*C.struct_kr_request)(ctx.data)
 		// Now we can use the C API as well
 		fmt.Printf("[go] resolved %d queries", C.list_size(&param.rplan.resolved))
 		return 0
diff --git a/modules/gostats/gostats.go b/modules/gostats/gostats.go
index 6a4d89b219f01600c3373afbaddafff5d52e1cf9..0c3b4ee61f0c0375889430cd79f7e2c9e87e4a6c 100644
--- a/modules/gostats/gostats.go
+++ b/modules/gostats/gostats.go
@@ -36,7 +36,7 @@ func Begin(ctx *C.knot_layer_t, param unsafe.Pointer) C.int {
 }
 
 func Finish(ctx *C.knot_layer_t) C.int {
-	var param *C.struct_kr_layer_param = (*C.struct_kr_layer_param)(ctx.data)
+	var param *C.struct_kr_request = (*C.struct_kr_request)(ctx.data)
 	fmt.Printf("[gostats] resolved %d queries", C.list_size(&param.rplan.resolved))
 	return 0
 }
diff --git a/modules/hints/hints.c b/modules/hints/hints.c
index 2fa3ef0da55ca0ea2926947417097525208a83da..a35356c128ffcdbe4fce50a5c58348c1bb94cce7 100644
--- a/modules/hints/hints.c
+++ b/modules/hints/hints.c
@@ -34,8 +34,8 @@
 
 /* Defaults */
 #define DEFAULT_FILE "/etc/hosts"
-#define DEBUG_MSG(fmt...) QRDEBUG(NULL, "hint",  fmt)
-typedef int (*rr_callback_t)(const knot_rrset_t *, unsigned, struct kr_layer_param *);
+#define DEBUG_MSG(qry, fmt...) QRDEBUG(qry, "hint",  fmt)
+typedef int (*rr_callback_t)(const knot_rrset_t *, unsigned, struct kr_request *);
 
 /** @todo Hack until layers can store userdata. */
 static struct kr_zonecut *g_map = NULL;
@@ -46,9 +46,11 @@ static int begin(knot_layer_t *ctx, void *module_param)
 	return ctx->state;
 }
 
-static int answer_query(pack_t *addr_set, struct kr_layer_param *param)
+static int answer_query(pack_t *addr_set, struct kr_request *param)
 {
-	struct kr_query *qry = kr_rplan_current(param->rplan);
+	struct kr_query *qry = kr_rplan_current(&param->rplan);
+	assert(qry);
+
 	knot_rrset_t rr;
 	knot_rrset_init(&rr, qry->sname, qry->stype, KNOT_CLASS_IN);
 	int family_len = sizeof(struct in_addr);
@@ -75,7 +77,7 @@ static int answer_query(pack_t *addr_set, struct kr_layer_param *param)
 	callback(&rr, 0, param);
 
 	/* Finalize */
-	DEBUG_MSG("<= answered from hints\n");
+	DEBUG_MSG(qry, "<= answered from hints\n");
 	knot_rdataset_clear(&rr.rrs, NULL);
 	qry->flags |= QUERY_RESOLVED;
 	return KNOT_STATE_DONE;
@@ -84,8 +86,8 @@ static int answer_query(pack_t *addr_set, struct kr_layer_param *param)
 static int query(knot_layer_t *ctx, knot_pkt_t *pkt)
 {
 	assert(pkt && ctx);
-	struct kr_layer_param *param = ctx->data;
-	struct kr_query *qry = kr_rplan_current(param->rplan);
+	struct kr_request *param = ctx->data;
+	struct kr_query *qry = kr_rplan_current(&param->rplan);
 	if (qry->stype != KNOT_RRTYPE_A && qry->stype != KNOT_RRTYPE_AAAA) {
 		return ctx->state;
 	}
@@ -149,7 +151,7 @@ static int load_map(struct kr_zonecut *hints, FILE *fp)
 		}
 	}
 
-	DEBUG_MSG("loaded %zu hints\n", count);
+	DEBUG_MSG(NULL, "loaded %zu hints\n", count);
 	return kr_ok();
 }
 
@@ -157,10 +159,10 @@ static int load(struct kr_module *module, const char *path)
 {
 	auto_fclose FILE *fp = fopen(path, "r");
 	if (fp == NULL) {
-		DEBUG_MSG("reading '%s' failed: %s\n", path, strerror(errno));
+		DEBUG_MSG(NULL, "reading '%s' failed: %s\n", path, strerror(errno));
 		return kr_error(errno);
 	} else {
-		DEBUG_MSG("reading '%s'\n", path);
+		DEBUG_MSG(NULL, "reading '%s'\n", path);
 	}
 
 	/* Create pool and copy itself */
diff --git a/tests/test_array.c b/tests/test_array.c
index 5151a545fa76473daf87480fa8e8be7913c0885a..8003700abf179eda13b3aee0036d8d5b51bcc504 100644
--- a/tests/test_array.c
+++ b/tests/test_array.c
@@ -46,6 +46,12 @@ static void test_array(void **state)
 		assert_true(ret == 0);
 	}
 
+	/* Overfill. */
+	for (unsigned i = 0; i < 4096; ++i) {
+		ret = array_push(arr, i);
+		assert_true(ret >= 0);
+	}
+
 	array_clear(arr);
 }
 
diff --git a/tests/test_cache.c b/tests/test_cache.c
index 86e79267521b3fdc1c4d3528f6698563d0060dbf..aaab55a4f2fad974b17cfb56db3382bf9e4f807e 100644
--- a/tests/test_cache.c
+++ b/tests/test_cache.c
@@ -26,7 +26,8 @@ knot_rrset_t global_rr;
 const char *global_env;
 
 #define CACHE_SIZE 10 * 4096
-#define CACHE_TTL 2
+#define CACHE_TTL 10
+#define CACHE_TIME 0
 
 /* Test invalid parameters. */
 static void test_invalid(void **state)
@@ -82,7 +83,7 @@ static void test_insert(void **state)
 	test_random_rr(&global_rr, CACHE_TTL);
 
 	namedb_txn_t *txn = test_txn_write(state);
-	int ret = kr_cache_insert(txn, &global_rr, 0);
+	int ret = kr_cache_insert(txn, &global_rr, CACHE_TIME);
 	if (ret == KNOT_EOK) {
 		ret = kr_cache_txn_commit(txn);
 	} else {
@@ -101,8 +102,9 @@ static void test_query(void **state)
 
 	namedb_txn_t *txn = test_txn_rdonly(state);
 
-	for (uint32_t timestamp = 0; timestamp < CACHE_TTL; ++timestamp) {
-		int query_ret = kr_cache_peek(txn, &cache_rr, &timestamp);
+	for (uint32_t timestamp = CACHE_TIME; timestamp < CACHE_TIME + CACHE_TTL; ++timestamp) {
+		uint32_t drift = timestamp;
+		int query_ret = kr_cache_peek(txn, &cache_rr, &drift);
 		bool rr_equal = knot_rrset_equal(&global_rr, &cache_rr, KNOT_RRSET_COMPARE_WHOLE);
 		assert_int_equal(query_ret, KNOT_EOK);
 		assert_true(rr_equal);
@@ -114,7 +116,7 @@ static void test_query(void **state)
 /* Test cache read (simulate aged entry) */
 static void test_query_aged(void **state)
 {
-	uint32_t timestamp = CACHE_TTL;
+	uint32_t timestamp = CACHE_TIME + CACHE_TTL;
 	knot_rrset_t cache_rr;
 	knot_rrset_init(&cache_rr, global_rr.owner, global_rr.type, global_rr.rclass);
 
@@ -124,6 +126,21 @@ static void test_query_aged(void **state)
 	kr_cache_txn_abort(txn);
 }
 
+/* Test cache removal */
+static void test_remove(void **state)
+{
+	uint32_t timestamp = CACHE_TIME;
+	knot_rrset_t cache_rr;
+	knot_rrset_init(&cache_rr, global_rr.owner, global_rr.type, global_rr.rclass);
+
+	namedb_txn_t *txn = test_txn_write(state);
+	int ret = kr_cache_remove(txn, &cache_rr);
+	assert_int_equal(ret, KNOT_EOK);
+	ret = kr_cache_peek(txn, &cache_rr, &timestamp);
+	assert_int_equal(ret, KNOT_ENOENT);
+	kr_cache_txn_commit(txn);
+}
+
 /* Test cache fill */
 static void test_fill(void **state)
 {
@@ -181,6 +198,8 @@ int main(void)
 	        unit_test(test_query),
 	        /* Cache aging */
 	        unit_test(test_query_aged),
+	        /* Removal */
+	        unit_test(test_remove),
 	        /* Cache fill */
 	        unit_test(test_fill),
 	        unit_test(test_clear),