diff --git a/src/libknot/nameserver/internet.c b/src/libknot/nameserver/internet.c
index bdbefad74241fe3985d53bcd18f24700cd32fb92..b91c13bac68104698f3c248afc453e83757dd790 100644
--- a/src/libknot/nameserver/internet.c
+++ b/src/libknot/nameserver/internet.c
@@ -30,7 +30,13 @@ enum {
 	MISS,    /* Negative result. */
 	DELEG,   /* Result is delegation. */
 	FOLLOW,  /* Resolution not complete (CNAME/DNAME chain). */
-	ERROR    /* Resolution failed. */
+	ERROR,   /* Resolution failed. */
+	TRUNC    /* Finished, but truncated. */
+};
+
+/*! \brief Features. */
+enum {
+	HAVE_DNSSEC = 1 << 0 /* DNSSEC both requested and supported. */
 };
 
 /*! \brief Check if given node was already visited. */
@@ -80,6 +86,94 @@ static int wildcard_list_cover(knot_pkt_t *pkt, struct query_data *qdata)
 	return ret;
 }
 
+/*! \brief DNSSEC both requested & available. */
+static bool have_dnssec(struct query_data *qdata)
+{
+	return qdata->flags & HAVE_DNSSEC;
+}
+
+/*! \brief Put RRSet and its RRSIG (if applicable) to packet. */
+static int put_answer_rr(knot_pkt_t *pkt, const knot_rrset_t *rr,
+			 const knot_dname_t *qname, struct query_data *qdata)
+{
+	/* RFC3123 s.6 - empty APL is valid, ignore other empty RRs. */
+	if (knot_rrset_rdata_rr_count(rr) < 1 &&
+	    knot_rrset_type(rr) != KNOT_RRTYPE_APL) {
+		return KNOT_EMALF;
+	}
+
+	uint16_t flags = 0;
+	uint16_t compr_hint = COMPR_HINT_NONE;
+
+	/* Wildcard expansion or exact match, either way RRSet owner is
+	 * is QNAME. We can fake name synthesis by setting compression hint to
+	 * QNAME position. Just need to check if we're answering QNAME and not
+	 * a CNAME target.
+	 */
+	if (pkt->rrset_count == 0) { /* Guaranteed first answer. */
+		compr_hint = COMPR_HINT_QNAME;
+	} else {
+		if (knot_dname_is_wildcard(rr->owner)) {
+			rr = ns_synth_from_wildcard(rr, qname);
+			flags |= KNOT_PF_FREE;
+		}
+	}
+
+	/*! \todo I don't like this much, RRSIGS could be added to whole section
+	 *        after processing I think. */
+	int ret = knot_pkt_put(pkt, compr_hint, rr, flags);
+	if (ret == KNOT_EOK && rr->rrsigs && have_dnssec(qdata)) {
+		ret = put_answer_rr(pkt, rr->rrsigs, qname, qdata);
+	}
+
+	return ret;
+}
+
+/*! \brief This is a wildcard-covered or any other terminal node for QNAME.
+ *         e.g. positive answer.
+ */
+static int put_answer_node(knot_pkt_t *pkt, uint16_t type,
+			   const knot_dname_t *qname, struct query_data *qdata)
+{
+	const knot_rrset_t *rrset = NULL;
+	knot_rrset_t **rrsets = knot_node_get_rrsets_no_copy(qdata->node);
+
+	int ret = KNOT_EOK;
+	switch (type) {
+	case KNOT_RRTYPE_ANY: /* Append all RRSets. */
+		/* If ANY not allowed, set TC bit. */
+		if (knot_zone_contents_any_disabled(qdata->zone->contents)) {
+			knot_wire_set_tc(pkt->wire);
+			return KNOT_EOK;
+		}
+		for (unsigned i = 0; i < knot_node_rrset_count(qdata->node); ++i) {
+			ret = put_answer_rr(pkt, rrsets[i], qname, qdata);
+			if (ret != KNOT_EOK) {
+				break;
+			}
+		}
+		break;
+	case KNOT_RRTYPE_RRSIG: /* Append all RRSIGs. */
+		for (unsigned i = 0; i < knot_node_rrset_count(qdata->node); ++i) {
+			if (rrsets[i]->rrsigs) {
+				ret = put_answer_rr(pkt, rrsets[i]->rrsigs, qname, qdata);
+				if (ret != KNOT_EOK) {
+					break;
+				}
+			}
+		}
+		break;
+	default: /* Single RRSet of given type. */
+		rrset = knot_node_get_rrset(qdata->node, type);
+		if (rrset) {
+			ret = put_answer_rr(pkt, rrset, qname, qdata);
+		}
+		break;
+	}
+
+	return ret;
+}
+
 static int follow_cname(knot_pkt_t *pkt, const knot_dname_t **name, struct query_data *qdata)
 {
 	dbg_ns("%s(%p, %p, %p)\n", __func__, name, pkt, qdata);
@@ -127,7 +221,11 @@ static int follow_cname(knot_pkt_t *pkt, const knot_dname_t **name, struct query
 		if (rr_to_add != cname_rr) {
 			knot_rrset_deep_free(&rr_to_add, 1);
 		}
-		return ERROR;
+		if (ret == KNOT_ESPACE) {
+			return TRUNC;
+		} else {
+			return ERROR;
+		}
 	} else {
 		/* Check if RR count increased. */
 		if (pkt->rrset_count <= rr_count_before) {
@@ -135,7 +233,6 @@ static int follow_cname(knot_pkt_t *pkt, const knot_dname_t **name, struct query
 			       __func__, rr_to_add);
 			return HIT;
 		}
-
 	}
 
 	/* Add RR signatures (from original RR). */
@@ -143,7 +240,11 @@ static int follow_cname(knot_pkt_t *pkt, const knot_dname_t **name, struct query
 	if (ret != KNOT_EOK) {
 		dbg_ns("%s: couldn't add rrsigs for CNAME RRSet %p\n",
 		       __func__, cname_rr);
-		return ERROR;
+		if (ret == KNOT_ESPACE) {
+			return TRUNC;
+		} else {
+			return ERROR;
+		}
 	}
 
 	/* Now follow the next CNAME TARGET. */
@@ -181,19 +282,20 @@ static int name_found(knot_pkt_t *pkt, const knot_dname_t **name,
 		return DELEG;
 	}
 
-	int added = 0; /*! \todo useless */
-	int ret = ns_put_answer(qdata->node, qdata->zone->contents, *name, qtype, pkt, &added, 0 /*! \todo check from pkt */);
-
+	uint16_t old_rrcount = pkt->rrset_count;
+	int ret = put_answer_node(pkt, qtype, *name, qdata);
 	if (ret != KNOT_EOK) {
-		dbg_ns("%s: failed answer from node %p (%d)\n", __func__, qdata->node, ret);
-		/*! \todo set rcode */
-		return ERROR;
-	} else {
-		dbg_ns("%s: answered, %d added\n", __func__, added);
+		dbg_ns("%s: failed answer from node %p (%s)\n",
+		       __func__, qdata->node, knot_strerror(ret));
+		if (ret == KNOT_ESPACE) {
+			return TRUNC;
+		} else {
+			return ERROR;
+		}
 	}
 
-	/* Check for NODATA. */
-	if (added == 0) {
+	/* Check for NODATA (=0 RRs added). */
+	if (old_rrcount == pkt->rrset_count) {
 		return NODATA;
 	} else {
 		return HIT;
@@ -228,7 +330,11 @@ static int name_not_found(knot_pkt_t *pkt, const knot_dname_t **name,
 		dbg_ns("%s: solving DNAME for name %p\n", __func__, *name);
 		int ret = ns_process_dname(dname_rrset, name, pkt);
 		if (ret != KNOT_EOK) {
-			return ERROR;
+			if (ret == KNOT_ESPACE) {
+				return TRUNC;
+			} else {
+				return ERROR;
+			}
 		}
 
 		return FOLLOW;
@@ -295,6 +401,7 @@ static int solve_authority(int state, const knot_dname_t **qname,
                            knot_pkt_t *pkt, struct query_data *qdata)
 {
 	int ret = KNOT_ERROR;
+	uint16_t qtype = knot_pkt_type(pkt);
 	const knot_zone_contents_t *zone_contents = qdata->zone->contents;
 
 	switch (state) {
@@ -304,8 +411,7 @@ static int solve_authority(int state, const knot_dname_t **qname,
 		 * But taking response size into consideration, DS/DNSKEY RRs
 		 * are rather large and may trigger fragmentation or even TCP
 		 * recovery. */
-		if (knot_pkt_qtype(qdata->pkt) != KNOT_RRTYPE_DS &&
-		    knot_pkt_qtype(qdata->pkt) != KNOT_RRTYPE_DNSKEY) {
+		if (qtype != KNOT_RRTYPE_DS && qtype != KNOT_RRTYPE_DNSKEY) {
 			ret = ns_put_authority_ns(zone_contents, pkt);
 			if (ret == KNOT_ESPACE) { /* Optional. */
 				ret = KNOT_EOK;
@@ -314,7 +420,7 @@ static int solve_authority(int state, const knot_dname_t **qname,
 			ret = KNOT_EOK;
 		}
 		/* Put NSEC/NSEC3 Wildcard proof if answered from wildcard. */
-		if (ret == KNOT_EOK && knot_pkt_have_dnssec(qdata->pkt)) {
+		if (ret == KNOT_EOK && have_dnssec(qdata)) {
 			ret = wildcard_list_cover(pkt, qdata);
 		}
 		break;
@@ -322,7 +428,7 @@ static int solve_authority(int state, const knot_dname_t **qname,
 		dbg_ns("%s: answer is NXDOMAIN\n", __func__);
 		qdata->rcode = KNOT_RCODE_NXDOMAIN;
 		ret = ns_put_authority_soa(zone_contents, pkt);
-		if (ret == KNOT_EOK && knot_pkt_have_dnssec(qdata->pkt)) {
+		if (ret == KNOT_EOK && have_dnssec(qdata)) {
 			ret = ns_put_nsec_nsec3_nxdomain(zone_contents,
 							 qdata->previous,
 							 qdata->encloser,
@@ -332,7 +438,7 @@ static int solve_authority(int state, const knot_dname_t **qname,
 	case NODATA: /* NODATA append AUTHORITY SOA + NSEC/NSEC3. */
 		dbg_ns("%s: answer is NODATA\n", __func__);
 		ret = ns_put_authority_soa(zone_contents, pkt);
-		if (ret == KNOT_EOK && knot_pkt_have_dnssec(qdata->pkt)) {
+		if (ret == KNOT_EOK && have_dnssec(qdata)) {
 			if (knot_dname_is_wildcard(qdata->node->owner)) {
 				ret = ns_put_nsec_nsec3_wildcard_nodata(qdata->node,
 									qdata->encloser,
@@ -346,11 +452,13 @@ static int solve_authority(int state, const knot_dname_t **qname,
 			}
 		}
 		break;
-	case DELEG:  /* Referral response. */ /*! \todo DS + NS */
-		ret = ns_referral(qdata->node, zone_contents, *qname, pkt, knot_pkt_qtype(pkt));
+	case DELEG:  /* Referral response. */
+		ret = ns_referral(qdata->node, zone_contents, *qname, pkt, qtype);
 		break;
-	case ERROR:
-		dbg_ns("%s: failed to resolve qname\n", __func__);
+	case TRUNC:  /* Truncated ANSWER. */
+		ret = KNOT_ESPACE;
+		break;
+	case ERROR:  /* Error resolving ANSWER. */
 		break;
 	default:
 		dbg_ns("%s: invalid state after qname processing = %d\n",
@@ -371,6 +479,12 @@ int internet_answer(knot_pkt_t *response, struct query_data *qdata)
 
 	NS_NEED_VALID_ZONE(qdata, KNOT_RCODE_REFUSED);
 
+	/* Check features - DNSSEC. */
+	if (knot_pkt_have_dnssec(qdata->pkt) &&
+	    knot_zone_contents_is_signed(qdata->zone->contents)) {
+		qdata->flags |= HAVE_DNSSEC;
+	}
+
 	/* Write answer RRs for QNAME. */
 	dbg_ns("%s: writing %p ANSWER\n", __func__, response);
 	knot_pkt_begin(response, KNOT_ANSWER);
@@ -385,7 +499,12 @@ int internet_answer(knot_pkt_t *response, struct query_data *qdata)
 	knot_pkt_begin(response, KNOT_AUTHORITY);
 	int ret = solve_authority(state, &qname, response, qdata);
 	if (ret != KNOT_EOK) {
-		return NS_PROC_FAIL;
+		/* Truncated. */
+		if (ret == KNOT_ESPACE) {
+			return NS_PROC_FINISH;
+		} else {
+			return NS_PROC_FAIL;
+		}
 
 	}
 
@@ -393,7 +512,8 @@ int internet_answer(knot_pkt_t *response, struct query_data *qdata)
 	dbg_ns("%s: writing %p ADDITIONAL\n", __func__, response);
 	knot_pkt_begin(response, KNOT_ADDITIONAL);
 	ret = ns_put_additional(response);
-	if (ret != KNOT_EOK) {
+	/* Optional section. */
+	if (ret != KNOT_EOK && ret != KNOT_ESPACE) {
 		return NS_PROC_FAIL;
 
 	}
diff --git a/src/libknot/nameserver/name-server.c b/src/libknot/nameserver/name-server.c
index 8e9cdd06d0daa8f0e1d091817d375c626fd15c30..5fd8c55d5139789a82bf59e7e83fc1db27b3ac35 100644
--- a/src/libknot/nameserver/name-server.c
+++ b/src/libknot/nameserver/name-server.c
@@ -220,187 +220,6 @@ int ns_add_rrsigs(knot_rrset_t *rrset, knot_pkt_t *resp,
 	return KNOT_EOK;
 }
 
-/*----------------------------------------------------------------------------*/
-/*!
- * \brief Retrieves RRSet(s) of given type from the given node and adds them to
- *        the response's Answer section.
- *
- * \param node Node where to take the RRSet from.
- * \param name Actual searched name (used in case of wildcard RRSet(s)).
- * \param type Type of the RRSet(s). If set to KNOT_RRTYPE_ANY, all RRSets
- *             from the node will be added to the answer.
- * \param resp Response where to add the RRSets.
- *
- * \return Number of RRSets added.
- */
-int ns_put_answer(const knot_node_t *node,
-                         const knot_zone_contents_t *zone,
-                         const knot_dname_t *name,
-                         uint16_t type, knot_pkt_t *resp, int *added,
-                         int check_any)
-{
-	*added = 0;
-dbg_ns_exec_verb(
-	char *name_str = knot_dname_to_str(node->owner);
-	dbg_ns_verb("Putting answers from node %s.\n", name_str);
-	free(name_str);
-);
-
-	int ret = KNOT_EOK;
-
-	switch (type) {
-	case KNOT_RRTYPE_ANY: {
-		dbg_ns_verb("Returning all RRTYPES.\n");
-
-		// if ANY not allowed, set TC bit
-		if (check_any && knot_zone_contents_any_disabled(zone)) {
-			knot_wire_set_tc(resp->wire);
-			break;
-		}
-
-		knot_rrset_t **rrsets = knot_node_get_rrsets(node);
-		if (rrsets == NULL) {
-			break;
-		}
-		int i = 0;
-		knot_rrset_t *rrset;
-		while (i < knot_node_rrset_count(node)) {
-			assert(rrsets[i] != NULL);
-			rrset = rrsets[i];
-
-			dbg_ns_detail("  Type: %u\n", knot_rrset_type(rrset));
-
-			if (knot_rrset_rdata_rr_count(rrset) > 0
-			    || knot_rrset_type(rrset) == KNOT_RRTYPE_APL) {
-
-				knot_rrset_t *rrset_orig = rrset;
-				ret = ns_check_wildcard(name, resp, &rrset);
-				if (ret != KNOT_EOK) {
-					dbg_ns("Failed to process wildcard.\n");
-					break;
-				}
-
-				unsigned flags = 0;
-				if (rrset != rrset_orig) {
-					flags |= KNOT_PF_FREE;
-				}
-
-				assert(KNOT_PKT_IN_AN(resp));
-				ret = knot_pkt_put(resp, 0, rrset, flags);
-				if (ret != KNOT_EOK) {
-					dbg_ns("Failed add Answer RRSet: %s\n",
-					       knot_strerror(ret));
-					break;
-				}
-
-				*added += 1;
-			}
-
-			assert(KNOT_PKT_IN_AN(resp));
-			ret = ns_add_rrsigs(rrset, resp, name, 1);
-			if (ret != KNOT_EOK) {
-				dbg_ns("Failed add RRSIGs for Answer RRSet: %s"
-				       "\n", knot_strerror(ret));
-				break;
-			}
-
-			*added += 1;
-
-			++i;
-		}
-		free(rrsets);
-		break;
-	}
-	case KNOT_RRTYPE_RRSIG: {
-		dbg_ns_verb("Returning all RRSIGs.\n");
-		knot_rrset_t **rrsets = knot_node_get_rrsets(node);
-		if (rrsets == NULL) {
-			break;
-		}
-		int i = 0;
-		int ret = 0;
-		knot_rrset_t *rrset;
-		while (i < knot_node_rrset_count(node)) {
-			assert(rrsets[i] != NULL);
-			rrset = knot_rrset_get_rrsigs(rrsets[i]);
-
-			if (rrset == NULL) {
-				++i;
-				continue;
-			}
-
-			knot_rrset_t *rrset_orig = rrset;
-			ret = ns_check_wildcard(name, resp, &rrset);
-			if (ret != KNOT_EOK) {
-				dbg_ns("Failed to process wildcard.\n");
-				break;
-			}
-
-			unsigned flags = 0;
-			if (rrset != rrset_orig) {
-				flags |= KNOT_PF_FREE;
-			}
-
-			assert(KNOT_PKT_IN_AN(resp));
-			ret = knot_pkt_put(resp, 0, rrset, flags);
-			if (ret != KNOT_EOK) {
-				dbg_ns("Failed add Answer RRSet: %s\n",
-				       knot_strerror(ret));
-				break;
-			}
-
-			*added += 1;
-			++i;
-		}
-		free(rrsets);
-		break;
-	}
-	default: {
-		int ret = 0;
-		knot_rrset_t *rrset = knot_node_get_rrset(node, type);
-		knot_rrset_t *rrset2 = rrset;
-		if (rrset != NULL && knot_rrset_rdata_rr_count(rrset)) {
-			dbg_ns_verb("Found RRSet of type %u\n", type);
-
-			knot_rrset_t *rrset2_orig = rrset2;
-			ret = ns_check_wildcard(name, resp, &rrset2);
-			if (ret != KNOT_EOK) {
-				dbg_ns("Failed to process wildcard.\n");
-				break;
-			}
-
-			unsigned flags = 0;
-			if (rrset2 != rrset2_orig) {
-				flags |= KNOT_PF_FREE;
-			}
-
-			assert(KNOT_PKT_IN_AN(resp));
-			ret = knot_pkt_put(resp, 0, rrset2, flags);
-			if (ret != KNOT_EOK) {
-				dbg_ns("Failed add Answer RRSet: %s\n",
-				       knot_strerror(ret));
-				break;
-			}
-
-			*added += 1;
-
-			assert(KNOT_PKT_IN_AN(resp));
-			ret = ns_add_rrsigs(rrset, resp, name, 1);
-
-			if (ret != KNOT_EOK) {
-				dbg_ns("Failed add RRSIGs for Answer RRSet: %s"
-				       "\n", knot_strerror(ret));
-				break;
-			}
-
-			*added += 1;
-		}
-	    }
-	}
-
-	return ret;
-}
-
 /*----------------------------------------------------------------------------*/
 
 static int ns_put_additional_rrset(knot_pkt_t *pkt, uint16_t compr_hint,
diff --git a/src/libknot/nameserver/name-server.h b/src/libknot/nameserver/name-server.h
index 4ef4f2f3d5a8e69797047c527e497ac383a3049f..ac58e172a8ddbb4fc57172a3fcbea1f9fa03a7dc 100644
--- a/src/libknot/nameserver/name-server.h
+++ b/src/libknot/nameserver/name-server.h
@@ -400,11 +400,7 @@ int ns_put_additional(knot_pkt_t *resp);
 
 int ns_put_authority_soa(const knot_zone_contents_t *zone,
                                  knot_pkt_t *resp);
-int ns_put_answer(const knot_node_t *node,
-                         const knot_zone_contents_t *zone,
-                         const knot_dname_t *name,
-                         uint16_t type, knot_pkt_t *resp, int *added,
-                         int check_any);
+
 int ns_put_authority_ns(const knot_zone_contents_t *zone,
                         knot_pkt_t *resp);
 int ns_referral(const knot_node_t *node,
diff --git a/src/libknot/nameserver/ns_proc_query.c b/src/libknot/nameserver/ns_proc_query.c
index fbe0bd374a6850eaec39154705f20a099548edfe..bc99aaf7467ebfd05869563c5a21ef0b5a3baa17 100644
--- a/src/libknot/nameserver/ns_proc_query.c
+++ b/src/libknot/nameserver/ns_proc_query.c
@@ -60,6 +60,7 @@ int ns_proc_query_reset(ns_proc_context_t *ctx)
 	knot_pkt_free(&data->pkt);
 	data->rcode = KNOT_RCODE_NOERROR;
 	data->rcode_tsig = 0;
+	data->flags = 0;
 	data->node = data->encloser = data->previous = NULL;
 
 	/* Free wildcard list. */
@@ -82,8 +83,8 @@ int ns_proc_query_in(knot_pkt_t *pkt, ns_proc_context_t *ctx)
 	struct query_data *data = QUERY_DATA(ctx);
 
 	/* Check query type. */
-	uint16_t query_type = knot_pkt_type(pkt);
-	switch(query_type) {
+	uint16_t pkt_type = knot_pkt_type(pkt);
+	switch(pkt_type) {
 	case KNOT_QUERY_NORMAL:
 	case KNOT_QUERY_NOTIFY:
 	case KNOT_QUERY_AXFR:
@@ -91,7 +92,7 @@ int ns_proc_query_in(knot_pkt_t *pkt, ns_proc_context_t *ctx)
 	case KNOT_QUERY_UPDATE:
 		break; /* Supported. */
 	default:
-		dbg_ns("%s: query_type(%hu) NOT SUPPORTED\n", __func__, query_type);
+		dbg_ns("%s: query_type(%hu) NOT SUPPORTED\n", __func__, pkt_type);
 		knot_pkt_free(&pkt);
 		return NS_PROC_NOOP; /* Refuse to process. */
 	}
diff --git a/src/libknot/nameserver/ns_proc_query.h b/src/libknot/nameserver/ns_proc_query.h
index 184ba1e13ff1b1648f98288086fc170af864ff42..ee7842086695cae7280c4677849d056f1354eaeb 100644
--- a/src/libknot/nameserver/ns_proc_query.h
+++ b/src/libknot/nameserver/ns_proc_query.h
@@ -43,6 +43,7 @@ enum ns_proc_query_flag {
 struct query_data {
 	uint16_t rcode;
 	uint16_t rcode_tsig;
+	uint16_t flags;
 	knot_pkt_t *pkt;
 	const knot_zone_t *zone; /*!< Associated zone. */
 	const knot_node_t *node, *encloser, *previous;