diff --git a/src/knot/ctl/remote.c b/src/knot/ctl/remote.c
index 8ebe9b508f47b0948280d477d9d8cc4e9149abcc..a28f2ad5ef485dc5f6ebf8f79936366b6ab635b4 100644
--- a/src/knot/ctl/remote.c
+++ b/src/knot/ctl/remote.c
@@ -668,7 +668,7 @@ int remote_process(server_t *s, conf_iface_t *ctl_if, int sock,
 			tsig_name = pkt->tsig_rr->owner;
 		}
 		acl_match_t *match = acl_find(conf()->ctl.acl, &ss, tsig_name);
-		knot_rcode_t ts_rc = 0;
+		uint16_t ts_rc = 0;
 		uint16_t ts_trc = 0;
 		uint64_t ts_tmsigned = 0;
 		if (match == NULL) {
diff --git a/src/knot/nameserver/axfr.c b/src/knot/nameserver/axfr.c
index d9125c77d77f16221927b36e498998038a6be38c..3878d773a7f9e13312b9747cdb381a6a2279ce54 100644
--- a/src/knot/nameserver/axfr.c
+++ b/src/knot/nameserver/axfr.c
@@ -224,7 +224,7 @@ int axfr_answer(knot_pkt_t *pkt, struct query_data *qdata)
 	}
 }
 
-int axfr_process_answer(knot_ns_xfr_t *xfr)
+int axfr_process_answer(knot_pkt_t *pkt, knot_ns_xfr_t *xfr)
 {
 	/*
 	 * Here we assume that 'xfr' contains TSIG information
@@ -234,7 +234,7 @@ int axfr_process_answer(knot_ns_xfr_t *xfr)
 
 	dbg_ns("ns_process_axfrin: incoming packet, wire size: %zu\n",
 	       xfr->wire_size);
-	int ret = xfrin_process_axfr_packet(xfr,
+	int ret = xfrin_process_axfr_packet(pkt, xfr,
 	                                    (knot_zone_contents_t **)&xfr->data);
 	if (ret > 0) { // transfer finished
 		dbg_ns("ns_process_axfrin: AXFR finished, zone created.\n");
diff --git a/src/knot/nameserver/axfr.h b/src/knot/nameserver/axfr.h
index 99d701a4c14bf68af6e8cf30e48ec1e22b8fa6f9..8bfb95a827798819213171791e30b37c3372f3e0 100644
--- a/src/knot/nameserver/axfr.h
+++ b/src/knot/nameserver/axfr.h
@@ -62,10 +62,11 @@ int axfr_answer(knot_pkt_t *pkt, struct query_data *qdata);
 /*!
  * \brief Processes an AXFR query response.
  *
+ * \param pkt Processed packet.
  * \param xfr Persistent transfer-specific data.
  *
  */
-int axfr_process_answer(knot_ns_xfr_t *xfr);
+int axfr_process_answer(knot_pkt_t *pkt, knot_ns_xfr_t *xfr);
 
 #endif /* _KNOT_AXFR_H_ */
 
diff --git a/src/knot/nameserver/ixfr.c b/src/knot/nameserver/ixfr.c
index f55188d35875aa5c6a2473c6ff88a69148b0aabf..83ffa1d9db42b53b55b26e33afc156d2da8ebc34 100644
--- a/src/knot/nameserver/ixfr.c
+++ b/src/knot/nameserver/ixfr.c
@@ -338,7 +338,7 @@ int ixfr_answer(knot_pkt_t *pkt, struct query_data *qdata)
 	return ret;
 }
 
-int ixfr_process_answer(knot_ns_xfr_t *xfr)
+int ixfr_process_answer(knot_pkt_t *pkt, knot_ns_xfr_t *xfr)
 {
 	dbg_ns("ns_process_ixfrin: incoming packet\n");
 
@@ -347,7 +347,7 @@ int ixfr_process_answer(knot_ns_xfr_t *xfr)
 	 * and the digest of the query sent to the master or the previous
 	 * digest.
 	 */
-	int ret = xfrin_process_ixfr_packet(xfr);
+	int ret = xfrin_process_ixfr_packet(pkt, xfr);
 
 	if (ret == XFRIN_RES_FALLBACK) {
 		dbg_ns("ns_process_ixfrin: Fallback to AXFR.\n");
diff --git a/src/knot/nameserver/ixfr.h b/src/knot/nameserver/ixfr.h
index 23b072d10292bdc397943fb98cc4a55199db4142..c8a680714a777d45012026b46d64bf29a675a041 100644
--- a/src/knot/nameserver/ixfr.h
+++ b/src/knot/nameserver/ixfr.h
@@ -45,6 +45,7 @@ int ixfr_answer(knot_pkt_t *pkt, struct query_data *qdata);
 /*!
  * \brief Process an IXFR query response.
  *
+ * \param pkt Processed packet.
  * \param xfr Persistent transfer-specific data.
  *
  * \retval KNOT_EOK If this packet was processed successfuly and another packet
@@ -64,7 +65,7 @@ int ixfr_answer(knot_pkt_t *pkt, struct query_data *qdata);
  * \retval Other If any other error occured. The connection should be closed.
  *
  */
-int ixfr_process_answer(knot_ns_xfr_t *xfr);
+int ixfr_process_answer(knot_pkt_t *pkt, knot_ns_xfr_t *xfr);
 
 #endif /* _KNOT_IXFR_H_ */
 
diff --git a/src/knot/nameserver/update.c b/src/knot/nameserver/update.c
index 7e79a6af71c9c91c8ab0a2f965bb8cb8894ee38d..ac3b18c134a18ac0e9d378b1d4a371d77436af8f 100644
--- a/src/knot/nameserver/update.c
+++ b/src/knot/nameserver/update.c
@@ -11,9 +11,7 @@
 #include "libknot/tsig-op.h"
 
 /* Forward decls. */
-static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
-                              knot_rcode_t *rcode, const struct sockaddr_storage *addr,
-                              knot_tsig_key_t *tsig_key);
+static int zones_process_update_auth(struct query_data *qdata);
 
 /* AXFR-specific logging (internal, expects 'qdata' variable set). */
 #define UPDATE_LOG(severity, msg...) \
@@ -93,14 +91,12 @@ static int update_prereq_check(struct query_data *qdata)
 	 * \note Permissions section means probably policies and fine grained
 	 *       access control, not transaction security.
 	 */
-	knot_rcode_t rcode = KNOT_RCODE_NOERROR;
 	knot_ddns_prereq_t *prereqs = NULL;
-	int ret = knot_ddns_process_prereqs(query, &prereqs, &rcode);
+	int ret = knot_ddns_process_prereqs(query, &prereqs, &qdata->rcode);
 	if (ret == KNOT_EOK) {
-		ret = knot_ddns_check_prereqs(contents, &prereqs, &rcode);
+		ret = knot_ddns_check_prereqs(contents, &prereqs, &qdata->rcode);
 		knot_ddns_prereqs_free(&prereqs);
 	}
-	qdata->rcode = rcode;
 
 	return ret;
 }
@@ -114,13 +110,7 @@ static int update_process(knot_pkt_t *resp, struct query_data *qdata)
 	}
 
 	/*! \todo Reusing the API for compatibility reasons. */
-	knot_rcode_t rcode = qdata->rcode;
-	ret = zones_process_update_auth((zone_t *)qdata->zone, qdata->query,
-	                                &rcode,
-	                                qdata->param->query_source,
-	                                qdata->sign.tsig_key);
-	qdata->rcode = rcode;
-	return ret;
+	return zones_process_update_auth(qdata);
 }
 
 int update_answer(knot_pkt_t *pkt, struct query_data *qdata)
@@ -189,11 +179,11 @@ int update_answer(knot_pkt_t *pkt, struct query_data *qdata)
  * NOTE: Mostly copied from xfrin_apply_changesets(). Should be refactored in
  *       order to get rid of duplicate code.
  */
-int knot_ns_process_update(const knot_pkt_t *query,
-                           knot_zone_contents_t *old_contents,
-                           knot_zone_contents_t **new_contents,
-                           knot_changesets_t *chgs, knot_rcode_t *rcode,
-                           uint32_t new_serial)
+static int knot_ns_process_update(const knot_pkt_t *query,
+                                  knot_zone_contents_t *old_contents,
+                                  knot_zone_contents_t **new_contents,
+                                  knot_changesets_t *chgs, uint16_t *rcode,
+                                  uint32_t new_serial)
 {
 	if (query == NULL || old_contents == NULL || chgs == NULL ||
 	    EMPTY_LIST(chgs->sets) || new_contents == NULL || rcode == NULL) {
@@ -268,14 +258,15 @@ static int replan_zone_sign_after_ddns(zone_t *zone, uint32_t refresh_at)
  * \retval KNOT_EOK if successful.
  * \retval error if not.
  */
-static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
-                              knot_rcode_t *rcode, const struct sockaddr_storage *addr,
-                              knot_tsig_key_t *tsig_key)
+static int zones_process_update_auth(struct query_data *qdata)
 {
-	assert(zone);
-	assert(query);
-	assert(rcode);
-	assert(addr);
+	assert(qdata);
+	assert(qdata->zone);
+
+	zone_t *zone = (zone_t *)qdata->zone;
+	conf_zone_t *zone_config = zone->conf;
+	knot_tsig_key_t *tsig_key = qdata->sign.tsig_key;
+	const struct sockaddr_storage *addr = qdata->param->query_source;
 
 	int ret = KNOT_EOK;
 
@@ -286,7 +277,7 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 	}
 	char *r_str = xfr_remote_str(addr, keytag);
 	char *msg  = sprintf_alloc("UPDATE of '%s' from %s",
-	                           zone->conf->name, r_str ? r_str : "'unknown'");
+	                           zone_config->name, r_str ? r_str : "'unknown'");
 	free(r_str);
 	free(keytag);
 
@@ -296,7 +287,7 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 	 */
 	knot_changesets_t *chgsets = knot_changesets_create();
 	if (chgsets == NULL) {
-		*rcode = KNOT_RCODE_SERVFAIL;
+		qdata->rcode = KNOT_RCODE_SERVFAIL;
 		log_zone_error("%s Cannot create changesets structure.\n", msg);
 		free(msg);
 		return ret;
@@ -308,18 +299,18 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 		free(msg);
 		return KNOT_ENOMEM;
 	}
-	*rcode = KNOT_RCODE_SERVFAIL; /* SERVFAIL unless it applies correctly. */
+	qdata->rcode = KNOT_RCODE_SERVFAIL; /* SERVFAIL unless it applies correctly. */
 
 	uint32_t new_serial = zones_next_serial(zone);
 
 	knot_zone_contents_t *new_contents = NULL;
 	knot_zone_contents_t *old_contents = zone->contents;
-	ret = knot_ns_process_update(query, old_contents, &new_contents,
-	                             chgsets, rcode, new_serial);
+	ret = knot_ns_process_update(qdata->query, old_contents, &new_contents,
+	                             chgsets, &qdata->rcode, new_serial);
 	if (ret != KNOT_EOK) {
 		if (ret > 0) {
 			log_zone_notice("%s: No change to zone made.\n", msg);
-			*rcode = KNOT_RCODE_NOERROR;
+			qdata->rcode = KNOT_RCODE_NOERROR;
 		}
 
 		knot_changesets_free(&chgsets);
@@ -331,12 +322,11 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 	knot_changeset_t *sec_ch = NULL;
 	uint32_t refresh_at = 0;
 
-	assert(zone->conf);
-	if (zone->conf->dnssec_enable) {
+	if (zone_config->dnssec_enable) {
 		sec_chs = knot_changesets_create();
 		sec_ch = knot_changesets_create_changeset(sec_chs);
 		if (sec_chs == NULL || sec_ch == NULL) {
-			xfrin_rollback_update(zone->contents, &new_contents,
+			xfrin_rollback_update(old_contents, &new_contents,
 			                      chgsets->changes);
 			knot_changesets_free(&chgsets);
 			free(msg);
@@ -346,7 +336,7 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 
 	// Apply changeset to zone created by DDNS processing
 
-	if (zone->conf->dnssec_enable) {
+	if (zone_config->dnssec_enable) {
 		/*!
 		 * Check if the UPDATE changed DNSKEYs. If yes, resign the whole
 		 * zone, if not, sign only the changeset.
@@ -354,13 +344,13 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 		 */
 		if (zones_dnskey_changed(old_contents, new_contents) ||
 		    zones_nsec3param_changed(old_contents, new_contents)) {
-			ret = knot_dnssec_zone_sign(new_contents, zone->conf,
+			ret = knot_dnssec_zone_sign(new_contents, zone_config,
 			                            sec_ch,
 			                            KNOT_SOA_SERIAL_KEEP,
 			                            &refresh_at, new_serial);
 		} else {
 			// Sign the created changeset
-			ret = knot_dnssec_sign_changeset(new_contents, zone->conf,
+			ret = knot_dnssec_sign_changeset(new_contents, zone_config,
 			                      knot_changesets_get_last(chgsets),
 			                      sec_ch, KNOT_SOA_SERIAL_KEEP,
 			                      &refresh_at,
@@ -370,7 +360,7 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 		if (ret != KNOT_EOK) {
 			log_zone_error("%s: Failed to sign incoming update (%s)"
 			               "\n", msg, knot_strerror(ret));
-			xfrin_rollback_update(zone->contents, &new_contents,
+			xfrin_rollback_update(old_contents, &new_contents,
 					      chgsets->changes);
 			knot_changesets_free(&chgsets);
 			knot_changesets_free(&sec_chs);
@@ -386,7 +376,7 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 	if (ret != KNOT_EOK) {
 		log_zone_error("%s: Failed to save new entry to journal (%s)\n",
 		               msg, knot_strerror(ret));
-		xfrin_rollback_update(zone->contents, &new_contents,
+		xfrin_rollback_update(old_contents, &new_contents,
 		                      chgsets->changes);
 		zones_free_merged_changesets(chgsets, sec_chs);
 		free(msg);
@@ -409,7 +399,7 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 		}
 
 		// Plan zone resign if needed
-		assert(zone->dnssec.timer);
+		assert(qdata->zone->dnssec.timer);
 		ret = replan_zone_sign_after_ddns(zone, refresh_at);
 		if (ret != KNOT_EOK) {
 			log_zone_error("%s: Failed to replan zone sign (%s)\n",
@@ -424,7 +414,7 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 		if (ret != KNOT_EOK) {
 			zones_store_changesets_rollback(transaction);
 			zones_free_merged_changesets(chgsets, sec_chs);
-			xfrin_rollback_update(zone->contents, &new_contents,
+			xfrin_rollback_update(old_contents, &new_contents,
 			                      chgsets->changes);
 			free(msg);
 			return ret;
@@ -437,7 +427,7 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 		if (ret != KNOT_EOK) {
 			log_zone_error("%s: Failed to commit new journal entry "
 			               "(%s).\n", msg, knot_strerror(ret));
-			xfrin_rollback_update(zone->contents, &new_contents,
+			xfrin_rollback_update(old_contents, &new_contents,
 			                      chgsets->changes);
 			zones_free_merged_changesets(chgsets, sec_chs);
 			free(msg);
@@ -450,16 +440,17 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 	rcu_read_unlock();      /* Unlock for switch. */
 	ret = xfrin_switch_zone(zone, new_contents, XFR_TYPE_UPDATE);
 	rcu_read_lock();        /* Relock */
-	zone_release(zone);     /* Release held pointer. */
 	if (ret != KNOT_EOK) {
 		log_zone_error("%s: Failed to replace current zone (%s)\n",
 		               msg, knot_strerror(ret));
 		// Cleanup old and new contents
-		xfrin_rollback_update(zone->contents, &new_contents,
+		xfrin_rollback_update(old_contents, &new_contents,
 		                      chgsets->changes);
 
 		/* Free changesets, but not the data. */
 		zones_free_merged_changesets(chgsets, sec_chs);
+		zone_release(zone);
+		qdata->zone = NULL;
 		return KNOT_ERROR;
 	}
 
@@ -472,7 +463,7 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 	// Free changesets, but not the data.
 	zones_free_merged_changesets(chgsets, sec_chs);
 	assert(ret == KNOT_EOK);
-	*rcode = KNOT_RCODE_NOERROR; /* Mark as successful. */
+	qdata->rcode = KNOT_RCODE_NOERROR; /* Mark as successful. */
 	if (new_signatures) {
 		log_zone_info("%s: Successfuly signed.\n", msg);
 	}
@@ -484,10 +475,16 @@ static int zones_process_update_auth(zone_t *zone, knot_pkt_t *query,
 	mem_trim();
 
 	/* Sync zonefile immediately if configured. */
-	if (zone->conf->dbsync_timeout == 0) {
+	if (zone_config->dbsync_timeout == 0) {
 		zones_schedule_zonefile_sync(zone, 0);
 	}
 
+	/* Since we unlocked RCU read lock, it is possible that the
+	 * zone was modified/removed in the background. Therefore,
+	 * we must NOT touch the zone after we release it here. */
+	zone_release(zone);
+	qdata->zone = NULL;
+
 	return ret;
 }
 
diff --git a/src/knot/server/xfr-handler.c b/src/knot/server/xfr-handler.c
index a3f1b9353548c73cf83963b12f69fe86419c6bb3..701d933cfdab5ee49079c2604c606d5648d66fcb 100644
--- a/src/knot/server/xfr-handler.c
+++ b/src/knot/server/xfr-handler.c
@@ -109,31 +109,6 @@ static int xfr_recv_tcp(int fd, struct sockaddr *addr, uint8_t *buf, size_t bufl
 static int xfr_recv_udp(int fd, struct sockaddr *addr, uint8_t *buf, size_t buflen)
 { return recv(fd, buf, buflen, 0); }
 
-/*! \todo This should be obsoleted by the generic response parsing layer. */
-static int parse_packet(knot_pkt_t *packet, knot_pkt_type_t *type)
-{
-	dbg_xfr("%s(%p, %p)\n", __func__, packet, type);
-	if (packet == NULL || type == NULL) {
-		return KNOT_EINVAL;
-	}
-
-	// 1) create empty response
-	int ret = KNOT_ERROR;
-	*type = KNOT_QUERY_INVALID;
-	if ((ret = knot_pkt_parse_question(packet)) != KNOT_EOK) {
-		dbg_xfr("%s: couldn't parse question = %d\n", __func__, ret);
-		return KNOT_RCODE_FORMERR;
-	}
-
-	// 2) determine the query type
-	*type = knot_pkt_type(packet);
-	if (*type == KNOT_QUERY_INVALID) {
-		return KNOT_RCODE_NOTIMPL;
-	}
-
-	return KNOT_RCODE_NOERROR;
-}
-
 /*! \brief Create forwarded query. */
 static int forward_packet(knot_ns_xfr_t *data, knot_pkt_t *pkt)
 {
@@ -203,11 +178,7 @@ static int xfr_task_setmsg(knot_ns_xfr_t *rq, const char *keytag)
 	}
 
 	/* Prepare log message. */
-	const char *zname = rq->zname;
-	if (zname == NULL) {
-		zname = rq->zone->conf->name;
-	}
-
+	const char *zname = rq->zone->conf->name;
 	rq->msg = sprintf_alloc(xd->name, zname, kstr ? kstr : "'unknown'");
 	free(kstr);
 	return KNOT_EOK;
@@ -289,7 +260,6 @@ static void xfr_task_cleanup(knot_ns_xfr_t *rq)
 	/* Cleanup other data - so that the structure may be reused. */
 	rq->packet_nr = 0;
 	rq->tsig_data_size = 0;
-	hattrie_clear(rq->lookup_tree);
 }
 
 /*! \brief End task properly and free it. */
@@ -545,7 +515,6 @@ static int xfr_async_finish(fdset_t *set, unsigned id)
 	switch(rq->type) {
 	case XFR_TYPE_AIN:
 	case XFR_TYPE_IIN:
-		rq->lookup_tree = hattrie_create();
 		if (ret != KNOT_EOK) {
 			pthread_mutex_lock(&zone->lock);
 			zone->xfr_in.state = XFR_IDLE;
@@ -654,62 +623,52 @@ static int xfr_task_finalize(knot_ns_xfr_t *rq)
 }
 
 /*! \brief Query response event handler function. */
-static int xfr_task_resp(xfrhandler_t *xfr, knot_ns_xfr_t *rq)
+static int xfr_task_resp(xfrhandler_t *xfr, knot_ns_xfr_t *rq, knot_pkt_t *pkt)
 {
-	knot_pkt_t *re = knot_pkt_new(rq->wire, rq->wire_size, NULL);
-	if (re == NULL) {
-		return KNOT_ENOMEM;
-	}
-
-	knot_pkt_type_t rt = KNOT_RESPONSE_NORMAL;
-	int ret = parse_packet(re, &rt);
-	if (ret != KNOT_EOK) {
-		knot_pkt_free(&re);
-		return KNOT_EOK; /* Ignore */
-	}
+	knot_pkt_type_t pkt_type = knot_pkt_type(pkt);
 
 	/* Ignore other packets. */
-	switch(rt) {
+	switch(pkt_type) {
 	case KNOT_RESPONSE_NORMAL:
 	case KNOT_RESPONSE_NOTIFY:
 	case KNOT_RESPONSE_UPDATE:
 		break;
 	default:
-		knot_pkt_free(&re);
+		knot_pkt_free(&pkt);
 		return KNOT_EOK; /* Ignore */
 	}
 
-	ret = knot_pkt_parse_payload(re, KNOT_PF_NO_MERGE);
+	int ret = knot_pkt_parse_payload(pkt, KNOT_PF_NO_MERGE);
 	if (ret != KNOT_EOK) {
-		knot_pkt_free(&re);
+		knot_pkt_free(&pkt);
 		return KNOT_EOK; /* Ignore */
 	}
 
 	/* Check TSIG. */
-	const knot_rrset_t * tsig_rr = re->tsig_rr;
+	const knot_rrset_t * tsig_rr = pkt->tsig_rr;
 	if (rq->tsig_key != NULL) {
-		ret = knot_tsig_client_check(tsig_rr, re->wire, re->size,
+		ret = knot_tsig_client_check(tsig_rr, pkt->wire, pkt->size,
 		                             rq->digest, rq->digest_size,
 		                             rq->tsig_key, 0);
 		if (ret != KNOT_EOK) {
 			log_zone_error("%s %s\n", rq->msg, knot_strerror(ret));
-			knot_pkt_free(&re);
+			knot_pkt_free(&pkt);
 			return KNOT_ECONNREFUSED;
 		}
 
 	}
 
 	/* Process response. */
-	switch(rt) {
+	switch(pkt_type) {
 	case KNOT_RESPONSE_NORMAL:
 		ret = zones_process_response(xfr->server, rq->packet_nr, &rq->addr,
-		                             re);
+		                             pkt);
 		break;
 	case KNOT_RESPONSE_NOTIFY:
-		ret = notify_process_response(re, rq->packet_nr);
+		ret = notify_process_response(pkt, rq->packet_nr);
 		break;
 	case KNOT_RESPONSE_UPDATE:
-		ret = forward_packet_response(rq, re);
+		ret = forward_packet_response(rq, pkt);
 		if (ret == KNOT_EOK) {
 			log_zone_info("%s Forwarded response.\n", rq->msg);
 		}
@@ -719,7 +678,6 @@ static int xfr_task_resp(xfrhandler_t *xfr, knot_ns_xfr_t *rq)
 		break;
 	}
 
-	knot_pkt_free(&re);
 	if (ret == KNOT_EUPTODATE) {  /* Check up-to-date zone. */
 		log_zone_info("%s %s (serial %u)\n", rq->msg,
 		              knot_strerror(ret),
@@ -769,16 +727,44 @@ static int xfr_fallback_axfr(knot_ns_xfr_t *rq)
 	return xfr_task_start(rq);
 }
 
-static int xfr_task_xfer(xfrhandler_t *xfr, knot_ns_xfr_t *rq)
+static int xfr_parse_packet(knot_pkt_t *pkt)
+{
+	/* This is important, don't merge RRs together. The SOAs are ordered
+	 * in a special way for a reason. */
+	int ret = knot_pkt_parse(pkt, KNOT_PF_NO_MERGE);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	// check if the response is OK
+	if (knot_wire_get_rcode(pkt->wire) != KNOT_RCODE_NOERROR) {
+		return KNOT_EXFRREFUSED;
+	}
+
+	// check if the TC bit is set (it must not be)
+	if (knot_wire_get_tc(pkt->wire)) {
+		return KNOT_EMALF;
+	}
+
+	return ret;
+}
+
+static int xfr_task_xfer(xfrhandler_t *xfr, knot_ns_xfr_t *rq, knot_pkt_t *pkt)
 {
+	/* Parse transfer packet. */
+	int ret = xfr_parse_packet(pkt);
+	if (ret != KNOT_EOK) {
+		log_zone_error("%s %s\n", rq->msg, knot_strerror(ret));
+		return ret;
+	}
+
 	/* Process incoming packet. */
-	int ret = KNOT_EOK;
 	switch(rq->type) {
 	case XFR_TYPE_AIN:
-		ret = axfr_process_answer(rq);
+		ret = axfr_process_answer(pkt, rq);
 		break;
 	case XFR_TYPE_IIN:
-		ret = ixfr_process_answer(rq);
+		ret = ixfr_process_answer(pkt, rq);
 		break;
 	default:
 		ret = KNOT_EINVAL;
@@ -792,7 +778,7 @@ static int xfr_task_xfer(xfrhandler_t *xfr, knot_ns_xfr_t *rq)
 		xfr_task_cleanup(rq);
 		rq->type = XFR_TYPE_AIN;
 		rq->msg[XFR_MSG_DLTTR] = 'A';
-		ret = axfr_process_answer(rq);
+		ret = axfr_process_answer(pkt, rq);
 	}
 
 	/* IXFR refused, try again with AXFR. */
@@ -876,15 +862,32 @@ static int xfr_process_event(xfrhandler_t *xfr, knot_ns_xfr_t *rq)
 		rq->wire_size = n;
 	}
 
-	/* Handle SOA/NOTIFY responses. */
+	/* Parse question. */
+	knot_pkt_t *pkt = knot_pkt_new(rq->wire, rq->wire_size, NULL);
+	if (pkt == NULL) {
+		return KNOT_ENOMEM;
+	}
+
+	int ret = knot_pkt_parse_question(pkt);
+	if (ret != KNOT_EOK) {
+		knot_pkt_free(&pkt);
+		return ret;
+	}
+
+	/* Process packet by request type. */
 	switch(rq->type) {
 	case XFR_TYPE_NOTIFY:
 	case XFR_TYPE_SOA:
 	case XFR_TYPE_FORWARD:
-		return xfr_task_resp(xfr, rq);
+		ret = xfr_task_resp(xfr, rq, pkt);
+		break;
 	default:
-		return xfr_task_xfer(xfr, rq);
+		ret = xfr_task_xfer(xfr, rq, pkt);
+		break;
 	}
+
+	knot_pkt_free(&pkt);
+	return ret;
 }
 
 /*! \brief Sweep inactive connection. */
@@ -1183,10 +1186,6 @@ int xfr_task_free(knot_ns_xfr_t *rq)
 		return KNOT_EINVAL;
 	}
 
-	/* Free DNAME trie. */
-	hattrie_free(rq->lookup_tree);
-	rq->lookup_tree = NULL;
-
 	/* Free TSIG buffers. */
 	free(rq->digest);
 	rq->digest = NULL;
diff --git a/src/knot/server/xfr-handler.h b/src/knot/server/xfr-handler.h
index e79241535cb1775aacfdde77540edb05a6c35cc7..c08358358dd41abb97a76c926fd0dae392710be6 100644
--- a/src/knot/server/xfr-handler.h
+++ b/src/knot/server/xfr-handler.h
@@ -99,7 +99,6 @@ typedef struct knot_ns_xfr {
 	struct sockaddr_storage addr, saddr;
 	knot_pkt_t *query;
 	knot_pkt_t *response;
-	knot_rcode_t rcode;
 	xfr_callback_t send;
 	xfr_callback_t recv;
 	int session;
@@ -119,7 +118,6 @@ typedef struct knot_ns_xfr {
 	size_t wire_maxlen;
 	void *data;
 	zone_t *zone;
-	char* zname;
 	knot_zone_contents_t *new_contents;
 	char *msg;
 
@@ -142,7 +140,6 @@ typedef struct knot_ns_xfr {
 	int fwd_src_fd;           /*!< Query originator fd. */
 	struct sockaddr_storage fwd_addr;
 
-	uint16_t tsig_rcode;
 	uint64_t tsig_prev_time_signed;
 
 	/*!
@@ -152,8 +149,6 @@ typedef struct knot_ns_xfr {
 	 * number counted from last TSIG check.
 	 */
 	int packet_nr;
-
-	hattrie_t *lookup_tree;
 } knot_ns_xfr_t;
 
 /*!
diff --git a/src/knot/server/zones.c b/src/knot/server/zones.c
index 70aaab305671dc7f3fb1c8f5dfb862b2a810a0ea..b663ee20f4d47b54a9b706fa6a7c01cec4d697dd 100644
--- a/src/knot/server/zones.c
+++ b/src/knot/server/zones.c
@@ -798,7 +798,7 @@ static int zones_serial_policy(const zone_t *zone)
 	return zone->conf->serial_policy;
 }
 
-uint32_t zones_next_serial(zone_t *zone)
+uint32_t zones_next_serial(const zone_t *zone)
 {
 	assert(zone);
 
@@ -1746,7 +1746,7 @@ void zones_schedule_zonefile_sync(zone_t *zone, uint32_t timeout)
 
 int zones_verify_tsig_query(const knot_pkt_t *query,
                             const knot_tsig_key_t *key,
-                            knot_rcode_t *rcode, uint16_t *tsig_rcode,
+                            uint16_t *rcode, uint16_t *tsig_rcode,
                             uint64_t *tsig_prev_time_signed)
 {
 	assert(key != NULL);
diff --git a/src/knot/server/zones.h b/src/knot/server/zones.h
index 60de09a47c4ed9cbb72631f38b6555b35fb3a491..c1a1321b386d56add54815be3e24a85353eda4e4 100644
--- a/src/knot/server/zones.h
+++ b/src/knot/server/zones.h
@@ -233,7 +233,7 @@ void zones_schedule_zonefile_sync(zone_t *zone, uint32_t timeout);
  */
 int zones_verify_tsig_query(const knot_pkt_t *query,
                             const knot_tsig_key_t *key,
-                            knot_rcode_t *rcode, uint16_t *tsig_rcode,
+                            uint16_t *rcode, uint16_t *tsig_rcode,
                             uint64_t *tsig_prev_time_signed);
 
 /*!
@@ -284,7 +284,7 @@ int zones_merge_and_store_changesets(zone_t *zone,
                                      journal_t **transaction);
 void zones_free_merged_changesets(knot_changesets_t *diff_chs,
                                   knot_changesets_t *sec_chs);
-uint32_t zones_next_serial(zone_t *zone);
+uint32_t zones_next_serial(const zone_t *zone);
 
 
 #endif // _KNOTD_ZONES_H_
diff --git a/src/knot/updates/ddns.c b/src/knot/updates/ddns.c
index bbcb4ce0ea5ad5c8cee98cd8122ad7057b3fefb1..85f56427741d43933532174cc9f6958ce8f95c73 100644
--- a/src/knot/updates/ddns.c
+++ b/src/knot/updates/ddns.c
@@ -202,7 +202,7 @@ static int knot_ddns_add_prereq(knot_ddns_prereq_t *prereqs,
 /*----------------------------------------------------------------------------*/
 
 static int knot_ddns_check_exist(const knot_zone_contents_t *zone,
-                                 const knot_rrset_t *rrset, knot_rcode_t *rcode)
+                                 const knot_rrset_t *rrset, uint16_t *rcode)
 {
 	assert(zone != NULL);
 	assert(rrset != NULL);
@@ -233,7 +233,7 @@ static int knot_ddns_check_exist(const knot_zone_contents_t *zone,
 
 static int knot_ddns_check_exist_full(const knot_zone_contents_t *zone,
                                       const knot_rrset_t *rrset,
-                                      knot_rcode_t *rcode)
+                                      uint16_t *rcode)
 {
 	assert(zone != NULL);
 	assert(rrset != NULL);
@@ -275,7 +275,7 @@ static int knot_ddns_check_exist_full(const knot_zone_contents_t *zone,
 
 static int knot_ddns_check_not_exist(const knot_zone_contents_t *zone,
                                      const knot_rrset_t *rrset,
-                                     knot_rcode_t *rcode)
+                                     uint16_t *rcode)
 {
 	assert(zone != NULL);
 	assert(rrset != NULL);
@@ -308,7 +308,7 @@ static int knot_ddns_check_not_exist(const knot_zone_contents_t *zone,
 
 static int knot_ddns_check_in_use(const knot_zone_contents_t *zone,
                                   const knot_dname_t *dname,
-                                  knot_rcode_t *rcode)
+                                  uint16_t *rcode)
 {
 	assert(zone != NULL);
 	assert(dname != NULL);
@@ -338,7 +338,7 @@ static int knot_ddns_check_in_use(const knot_zone_contents_t *zone,
 
 static int knot_ddns_check_not_in_use(const knot_zone_contents_t *zone,
                                       const knot_dname_t *dname,
-                                      knot_rcode_t *rcode)
+                                      uint16_t *rcode)
 {
 	assert(zone != NULL);
 	assert(dname != NULL);
@@ -368,7 +368,7 @@ static int knot_ddns_check_not_in_use(const knot_zone_contents_t *zone,
 /*----------------------------------------------------------------------------*/
 
 int knot_ddns_check_zone(const knot_zone_contents_t *zone,
-                         const knot_pkt_t *query, knot_rcode_t *rcode)
+                         const knot_pkt_t *query, uint16_t *rcode)
 {
 	if (zone == NULL || query == NULL || rcode == NULL) {
 		if (rcode != NULL) {
@@ -395,7 +395,7 @@ int knot_ddns_check_zone(const knot_zone_contents_t *zone,
 /*----------------------------------------------------------------------------*/
 
 int knot_ddns_process_prereqs(const knot_pkt_t *query,
-                              knot_ddns_prereq_t **prereqs, knot_rcode_t *rcode)
+                              knot_ddns_prereq_t **prereqs, uint16_t *rcode)
 {
 	if (query == NULL || prereqs == NULL || rcode == NULL) {
 		return KNOT_EINVAL;
@@ -433,7 +433,7 @@ int knot_ddns_process_prereqs(const knot_pkt_t *query,
 /*----------------------------------------------------------------------------*/
 
 int knot_ddns_check_prereqs(const knot_zone_contents_t *zone,
-                            knot_ddns_prereq_t **prereqs, knot_rcode_t *rcode)
+                            knot_ddns_prereq_t **prereqs, uint16_t *rcode)
 {
 	int i, ret;
 
@@ -491,7 +491,7 @@ int knot_ddns_check_prereqs(const knot_zone_contents_t *zone,
 
 static int knot_ddns_check_update(const knot_rrset_t *rrset,
                                   const knot_pkt_t *query,
-                                  knot_rcode_t *rcode)
+                                  uint16_t *rcode)
 {
 	/* Accept both subdomain and dname match. */
 	dbg_ddns("Checking UPDATE packet.\n");
@@ -1636,7 +1636,7 @@ int knot_ddns_process_update(knot_zone_contents_t *zone,
                              const knot_pkt_t *query,
                              knot_changeset_t *changeset,
                              knot_changes_t *changes,
-                             knot_rcode_t *rcode, uint32_t new_serial)
+                             uint16_t *rcode, uint32_t new_serial)
 {
 	if (zone == NULL || query == NULL || changeset == NULL || rcode == NULL
 	    || changes == NULL) {
diff --git a/src/knot/updates/ddns.h b/src/knot/updates/ddns.h
index da3645006fc5b98296d6929b645df964d1be01b6..f6993b65bf3a3a1ec47462bd7a6205083a99aaf8 100644
--- a/src/knot/updates/ddns.h
+++ b/src/knot/updates/ddns.h
@@ -58,19 +58,19 @@ typedef struct knot_ddns_prereq_t {
 } knot_ddns_prereq_t;
 
 int knot_ddns_check_zone(const knot_zone_contents_t *zone,
-                         const knot_pkt_t *query, knot_rcode_t *rcode);
+                         const knot_pkt_t *query, uint16_t *rcode);
 
 int knot_ddns_process_prereqs(const knot_pkt_t *query,
-                              knot_ddns_prereq_t **prereqs, knot_rcode_t *rcode);
+                              knot_ddns_prereq_t **prereqs, uint16_t *rcode);
 
 int knot_ddns_check_prereqs(const knot_zone_contents_t *zone,
-                            knot_ddns_prereq_t **prereqs, knot_rcode_t *rcode);
+                            knot_ddns_prereq_t **prereqs, uint16_t *rcode);
 
 int knot_ddns_process_update(knot_zone_contents_t *zone,
                               const knot_pkt_t *query,
                               knot_changeset_t *changeset,
                               knot_changes_t *changes,
-                              knot_rcode_t *rcode, uint32_t new_serial);
+                              uint16_t *rcode, uint32_t new_serial);
 
 void knot_ddns_prereqs_free(knot_ddns_prereq_t **prereq);
 
diff --git a/src/knot/updates/xfr-in.c b/src/knot/updates/xfr-in.c
index 584a77c75bbe6676b8c446f9d7db681523a18e14..f862934685e100084e2a0a74d098e386e164beb1 100644
--- a/src/knot/updates/xfr-in.c
+++ b/src/knot/updates/xfr-in.c
@@ -223,50 +223,11 @@ static int xfrin_check_tsig(knot_pkt_t *packet, knot_ns_xfr_t *xfr,
 
 /*----------------------------------------------------------------------------*/
 
-static int xfrin_parse(knot_pkt_t **dst, uint8_t *wire, size_t wire_size)
-{
-	assert(dst != NULL);
-
-	int ret = KNOT_EOK;
-	knot_pkt_t *pkt = knot_pkt_new(wire, wire_size, NULL);
-	if (pkt == NULL) {
-		knot_pkt_free(&pkt);
-		return KNOT_ENOMEM;
-	}
-
-	/* This is important, don't merge RRs together. The SOAs are ordered
-	 * in a special way for a reason. */
-	ret = knot_pkt_parse(pkt, KNOT_PF_NO_MERGE);
-	if (ret != KNOT_EOK) {
-		knot_pkt_free(&pkt);
-		return ret;
-	}
-
-	// check if the response is OK
-	if (knot_wire_get_rcode(pkt->wire) != KNOT_RCODE_NOERROR) {
-		knot_pkt_free(&pkt);
-		return KNOT_EXFRREFUSED;
-	}
-
-	// check if the TC bit is set (it must not be)
-	if (knot_wire_get_tc(pkt->wire)) {
-		knot_pkt_free(&pkt);
-		return KNOT_EMALF;
-	}
-
-	*dst = pkt;
-	return KNOT_EOK;
-}
-
 static int xfrin_take_rr(const knot_pktsection_t *answer, knot_rrset_t **rr, uint16_t *cur)
 {
 	int ret = KNOT_EOK;
 	if (*cur < answer->count) {
-		/*! \note For now, the RRSets are allocated using malloc(),
-		 *        clearing the KNOT_PF_FREE flags takes their ownership.
-		 */
-		*rr = (knot_rrset_t *)answer->rr[*cur];
-		answer->rrinfo[*cur].flags &= ~KNOT_PF_FREE;
+		ret = knot_rrset_copy(answer->rr[*cur], rr, NULL);
 		*cur += 1;
 	} else {
 		*rr = NULL;
@@ -278,23 +239,17 @@ static int xfrin_take_rr(const knot_pktsection_t *answer, knot_rrset_t **rr, uin
 
 /*----------------------------------------------------------------------------*/
 
-int xfrin_process_axfr_packet(knot_ns_xfr_t *xfr, knot_zone_contents_t **zone)
+int xfrin_process_axfr_packet(knot_pkt_t *pkt, knot_ns_xfr_t *xfr, knot_zone_contents_t **zone)
 {
-	if (xfr->wire == NULL) {
+	if (pkt == NULL) {
 		return KNOT_EINVAL;
 	}
 
-	knot_pkt_t *packet = NULL;
-	int ret = xfrin_parse(&packet, xfr->wire, xfr->wire_size);
-	if (ret != KNOT_EOK ) {
-		return ret;
-	}
-
 	uint16_t rr_id = 0;
 	knot_rrset_t *rr = NULL;
-	const knot_pktsection_t *answer = knot_pkt_section(packet, KNOT_ANSWER);
+	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
 
-	ret = xfrin_take_rr(answer, &rr, &rr_id);
+	int ret = xfrin_take_rr(answer, &rr, &rr_id);
 	if (*zone == NULL) {
 		// Transfer start, init zone
 		if (rr->type != KNOT_RRTYPE_SOA) {
@@ -302,7 +257,6 @@ int xfrin_process_axfr_packet(knot_ns_xfr_t *xfr, knot_zone_contents_t **zone)
 		}
 		*zone = knot_zone_contents_new(rr->owner);
 		if (*zone == NULL) {
-			knot_pkt_free(&packet);
 			return KNOT_ENOMEM;
 		}
 		xfr->packet_nr = 0;
@@ -318,8 +272,7 @@ int xfrin_process_axfr_packet(knot_ns_xfr_t *xfr, knot_zone_contents_t **zone)
 		if (rr->type == KNOT_RRTYPE_SOA &&
 		    knot_node_rrset(zc.z->apex, KNOT_RRTYPE_SOA)) {
 			// Last SOA, last message, check TSIG.
-			ret = xfrin_check_tsig(packet, xfr, 1);
-			knot_pkt_free(&packet);
+			ret = xfrin_check_tsig(pkt, xfr, 1);
 			knot_rrset_free(&rr, NULL);
 			if (ret != KNOT_EOK) {
 				return ret;
@@ -328,7 +281,6 @@ int xfrin_process_axfr_packet(knot_ns_xfr_t *xfr, knot_zone_contents_t **zone)
 		} else {
 			ret = zcreator_step(&zc, rr);
 			if (ret != KNOT_EOK) {
-				knot_pkt_free(&packet);
 				knot_rrset_free(&rr, NULL);
 				return ret;
 			}
@@ -338,34 +290,26 @@ int xfrin_process_axfr_packet(knot_ns_xfr_t *xfr, knot_zone_contents_t **zone)
 
 	assert(rr == NULL);
 	// Check possible TSIG at the end of DNS message.
-	ret = xfrin_check_tsig(packet, xfr,
+	ret = xfrin_check_tsig(pkt, xfr,
 	                       knot_ns_tsig_required(xfr->packet_nr));
-	knot_pkt_free(&packet);
 	return ret; // ret == KNOT_EOK means processing continues.
 }
 
 /*----------------------------------------------------------------------------*/
 
-int xfrin_process_ixfr_packet(knot_ns_xfr_t *xfr)
+int xfrin_process_ixfr_packet(knot_pkt_t *pkt, knot_ns_xfr_t *xfr)
 {
 	knot_changesets_t **chs = (knot_changesets_t **)(&xfr->data);
-	if (xfr->wire == NULL || chs == NULL) {
+	if (pkt == NULL || chs == NULL) {
 		dbg_xfrin("Wrong parameters supported.\n");
 		return KNOT_EINVAL;
 	}
 
-	knot_pkt_t *packet = NULL;
-	int ret = xfrin_parse(&packet, xfr->wire, xfr->wire_size);
-	if (ret != KNOT_EOK ) {
-		return ret;
-	}
-
 	uint16_t rr_id = 0;
 	knot_rrset_t *rr = NULL;
-	const knot_pktsection_t *answer = knot_pkt_section(packet, KNOT_ANSWER);
-	ret = xfrin_take_rr(answer, &rr, &rr_id);
+	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
+	int ret = xfrin_take_rr(answer, &rr, &rr_id);
 	if (ret != KNOT_EOK) {
-		knot_pkt_free(&packet);
 		return KNOT_EXFRREFUSED; /* Empty, try again with AXFR */
 	}
 
@@ -382,7 +326,6 @@ int xfrin_process_ixfr_packet(knot_ns_xfr_t *xfr)
 		ret = knot_changesets_init(chs);
 		if (ret != KNOT_EOK) {
 			knot_rrset_free(&rr, NULL);
-			knot_pkt_free(&packet);
 			return ret;
 		}
 
@@ -421,11 +364,9 @@ int xfrin_process_ixfr_packet(knot_ns_xfr_t *xfr)
 		 */
 		if (rr == NULL) {
 			dbg_xfrin("Response containing only SOA,\n");
-			knot_pkt_free(&packet);
 			return XFRIN_RES_SOA_ONLY;
 		} else if (knot_rrset_type(rr) != KNOT_RRTYPE_SOA) {
 			knot_rrset_free(&rr, NULL);
-			knot_pkt_free(&packet);
 			dbg_xfrin("Fallback to AXFR.\n");
 			ret = XFRIN_RES_FALLBACK;
 			return ret;
@@ -538,11 +479,10 @@ dbg_xfrin_exec_verb(
 				/*! \note [TSIG] Check TSIG, we're at the end of
 				 *               transfer.
 				 */
-				ret = xfrin_check_tsig(packet, xfr, 1);
+				ret = xfrin_check_tsig(pkt, xfr, 1);
 
 				// last SOA, discard and end
 				knot_rrset_free(&rr, NULL);
-				knot_pkt_free(&packet);
 
 				/*! \note [TSIG] If TSIG validates, consider
 				 *        transfer complete. */
@@ -624,7 +564,7 @@ dbg_xfrin_exec_verb(
 	/*! \note Check TSIG, we're at the end of packet. It may not be
 	 *        required.
 	 */
-	ret = xfrin_check_tsig(packet, xfr,
+	ret = xfrin_check_tsig(pkt, xfr,
 			       knot_ns_tsig_required(xfr->packet_nr));
 	dbg_xfrin_verb("xfrin_check_tsig() returned %d\n", ret);
 	++xfr->packet_nr;
@@ -636,7 +576,6 @@ dbg_xfrin_exec_verb(
 
 	// here no RRs remain in the packet but the transfer is not finished
 	// yet, return EOK
-	knot_pkt_free(&packet);
 	return KNOT_EOK;
 
 cleanup:
@@ -645,7 +584,6 @@ cleanup:
 
 	dbg_xfrin_detail("Cleanup after processing IXFR/IN packet.\n");
 	knot_changesets_free(chs);
-	knot_pkt_free(&packet);
 	xfr->data = 0;
 	return ret;
 }
diff --git a/src/knot/updates/xfr-in.h b/src/knot/updates/xfr-in.h
index 474d2a82d3f81869e5d012f7e926f4c2e85e0c35..3a7f20654d2c9aac32c601e12c09a02abc809cd6 100644
--- a/src/knot/updates/xfr-in.h
+++ b/src/knot/updates/xfr-in.h
@@ -114,7 +114,7 @@ int xfrin_create_ixfr_query(const zone_t *zone, knot_pkt_t *pkt);
  *
  * \todo Refactor!!!
  */
-int xfrin_process_axfr_packet(knot_ns_xfr_t *xfr, knot_zone_contents_t **zone);
+int xfrin_process_axfr_packet(knot_pkt_t *pkt, knot_ns_xfr_t *xfr, knot_zone_contents_t **zone);
 
 /*!
  * \brief Destroys the whole changesets structure.
@@ -138,7 +138,7 @@ void xfrin_free_changesets(knot_changesets_t **changesets);
  * \retval KNOT_EMALF
  * \retval KNOT_ENOMEM
  */
-int xfrin_process_ixfr_packet(knot_ns_xfr_t *xfr);
+int xfrin_process_ixfr_packet(knot_pkt_t *pkt, knot_ns_xfr_t *xfr);
 
 /*!
  * \brief Applies changesets *with* zone shallow copy.