diff --git a/src/knot/dnssec/nsec3-chain.c b/src/knot/dnssec/nsec3-chain.c
index 41a4cff6de6a64a4982c9c7bde91952b9f560ad3..c3e8f52bcce42c17f4c191ccd3cdcc175a995d25 100644
--- a/src/knot/dnssec/nsec3-chain.c
+++ b/src/knot/dnssec/nsec3-chain.c
@@ -121,7 +121,7 @@ static int shallow_copy_signature(const zone_node_t *from, zone_node_t *to)
 	if (knot_rrset_empty(&from_sig)) {
 		return KNOT_EOK;
 	}
-	return node_add_rrset(to, &from_sig, NULL);
+	return node_add_rrset(to, &from_sig);
 }
 
 /*!
@@ -298,7 +298,7 @@ static zone_node_t *create_nsec3_node(knot_dname_t *owner,
 		return NULL;
 	}
 
-	ret = node_add_rrset(new_node, &nsec3_rrset, NULL);
+	ret = node_add_rrset(new_node, &nsec3_rrset);
 	knot_rrset_clear(&nsec3_rrset, NULL);
 	if (ret != KNOT_EOK) {
 		node_free(&new_node);
diff --git a/src/knot/nameserver/update.c b/src/knot/nameserver/update.c
index 3c034c43e57659affce83570e2361761b258a029..d676d29757f213d1baf4d5dab2fe17cd4d981c77 100644
--- a/src/knot/nameserver/update.c
+++ b/src/knot/nameserver/update.c
@@ -281,7 +281,11 @@ static int zones_process_update_auth(struct query_data *qdata)
 		ret = xfrin_apply_changesets(zone, chgsets, &new_contents);
 		if (ret != KNOT_EOK) {
 			log_zone_notice("%s: Failed to process: %s.\n", msg, knot_strerror(ret));
-			qdata->rcode = KNOT_RCODE_SERVFAIL;
+			if (ret == KNOT_ETTL) {
+				qdata->rcode = KNOT_RCODE_REFUSED;
+			} else {
+				qdata->rcode = KNOT_RCODE_SERVFAIL;
+			}
 			knot_changesets_free(&chgsets);
 			free(msg);
 			return ret;
diff --git a/src/knot/updates/ddns.c b/src/knot/updates/ddns.c
index 0c9ade64d8322c779904315dab4fb1bb1d5188f3..059f4a72524bb69702c6bb4e1e1863f004c33ea6 100644
--- a/src/knot/updates/ddns.c
+++ b/src/knot/updates/ddns.c
@@ -667,18 +667,6 @@ static int process_add_normal(const zone_node_t *node,
 		return KNOT_EOK;
 	}
 
-	/* First check if the TTL of the new RR is equal to that of the first
-	 * RR in the node's RRSet. If not, refuse the UPDATE.
-	 */
-	knot_rrset_t rr_in_zone = node_rrset(node, rr->type);
-	if (node_rrtype_exists(node, rr->type)) {
-		const knot_rdata_t *add_data = knot_rdataset_at(&rr->rrs, 0);
-		const knot_rdata_t *zone_data = knot_rdataset_at(&rr_in_zone.rrs, 0);
-		if (knot_rdata_ttl(add_data) != knot_rdata_ttl(zone_data)) {
-			return KNOT_ETTL;
-		}
-	}
-
 	const bool apex_ns = node_rrtype_exists(node, KNOT_RRTYPE_SOA) &&
 	                     rr->type == KNOT_RRTYPE_NS;
 	return add_rr_to_chgset(rr, changeset, apex_ns ? apex_ns_rem : NULL);
@@ -931,7 +919,7 @@ static uint16_t ret_to_rcode(int ret)
 {
 	if (ret == KNOT_EMALF) {
 		return KNOT_RCODE_FORMERR;
-	} else if (ret == KNOT_EDENIED || ret == KNOT_ETTL) {
+	} else if (ret == KNOT_EDENIED) {
 		return KNOT_RCODE_REFUSED;
 	} else {
 		return KNOT_RCODE_SERVFAIL;
diff --git a/src/knot/updates/xfr-in.c b/src/knot/updates/xfr-in.c
index 41d8d63d5f0f67ee9df710f1767d111ee23b87f8..5e5e493c6430a43165496808f0a98007f9626169 100644
--- a/src/knot/updates/xfr-in.c
+++ b/src/knot/updates/xfr-in.c
@@ -826,7 +826,7 @@ static int xfrin_apply_remove(knot_zone_contents_t *contents,
 }
 
 static int add_rr(zone_node_t *node, const knot_rrset_t *rr,
-                  knot_changeset_t *chset)
+                  knot_changeset_t *chset, bool master)
 {
 	knot_rrset_t changed_rrset = node_rrset(node, rr->type);
 	if (!knot_rrset_empty(&changed_rrset)) {
@@ -843,37 +843,24 @@ static int add_rr(zone_node_t *node, const knot_rrset_t *rr,
 			clear_new_rrs(node, rr->type);
 			return ret;
 		}
-
-		// Extract copy, merge into it
-		knot_rdataset_t *changed_rrs = node_rdataset(node, rr->type);
-		ret = knot_rdataset_merge(changed_rrs, &rr->rrs, NULL);
-		if (ret != KNOT_EOK) {
-			clear_new_rrs(node, rr->type);
-			return ret;
-		}
-	} else {
-		// Inserting new RRSet, data will be copied.
-		bool ttl_err = false;
-		int ret = node_add_rrset(node, rr, &ttl_err);
-		if (ret != KNOT_EOK) {
+	}
+	// Insert new RR to RRSet, data will be copied.
+	int ret = node_add_rrset(node, rr);
+	if (ret != KNOT_EOK) {
+		if (ret == KNOT_ETTL) {
+			log_ttl_error(node, rr);
+			if (master) {
+				// TTL errors fatal on master.
+				return KNOT_ETTL;
+			}
+		} else {
 			return ret;
 		}
-
-		if (ttl_err) {
-			char type_str[16] = { '\0' };
-			knot_rrtype_to_string(rr->type, type_str, sizeof(type_str));
-			char *name = knot_dname_to_str(rr->owner);
-			char *zname = knot_dname_to_str(chset->soa_from->owner);
-			log_zone_warning("Changes application to zone %s: TTL mismatch"
-			                 " in %s, type %s\n", zname, name, type_str);
-			free(name);
-			free(zname);
-		}
 	}
 
 	// Get changed RRS and store for possible rollback.
 	knot_rdataset_t *rrs = node_rdataset(node, rr->type);
-	int ret = add_new_data(chset, rrs->data);
+	ret = add_new_data(chset, rrs->data);
 	if (ret != KNOT_EOK) {
 		knot_rdataset_clear(rrs, NULL);
 		return ret;
@@ -883,7 +870,7 @@ static int add_rr(zone_node_t *node, const knot_rrset_t *rr,
 }
 
 static int xfrin_apply_add(knot_zone_contents_t *contents,
-                           knot_changeset_t *chset)
+                           knot_changeset_t *chset, bool master)
 {
 	knot_rr_ln_t *rr_node = NULL;
 	WALK_LIST(rr_node, chset->add) {
@@ -895,7 +882,7 @@ static int xfrin_apply_add(knot_zone_contents_t *contents,
 			return KNOT_ENOMEM;
 		}
 
-		int ret = add_rr(node, rr, chset);
+		int ret = add_rr(node, rr, chset, master);
 		if (ret != KNOT_EOK) {
 			return ret;
 		}
@@ -917,14 +904,13 @@ static int xfrin_apply_replace_soa(knot_zone_contents_t *contents,
 
 	assert(!node_rrtype_exists(contents->apex, KNOT_RRTYPE_SOA));
 
-	return add_rr(contents->apex, chset->soa_to, chset);
+	return add_rr(contents->apex, chset->soa_to, chset, false);
 }
 
 /*----------------------------------------------------------------------------*/
 
-static int xfrin_apply_changeset(list_t *old_rrs, list_t *new_rrs,
-                                 knot_zone_contents_t *contents,
-                                 knot_changeset_t *chset)
+static int xfrin_apply_changeset(knot_zone_contents_t *contents,
+                                 knot_changeset_t *chset, bool master)
 {
 	/*
 	 * Applies one changeset to the zone. Checks if the changeset may be
@@ -948,7 +934,7 @@ static int xfrin_apply_changeset(list_t *old_rrs, list_t *new_rrs,
 		return ret;
 	}
 
-	ret = xfrin_apply_add(contents, chset);
+	ret = xfrin_apply_add(contents, chset, master);
 	if (ret != KNOT_EOK) {
 		return ret;
 	}
@@ -1160,9 +1146,8 @@ int xfrin_apply_changesets_directly(knot_zone_contents_t *contents,
 
 	knot_changeset_t *set = NULL;
 	WALK_LIST(set, chsets->sets) {
-		int ret = xfrin_apply_changeset(&set->old_data,
-		                                &set->new_data,
-		                                contents, set);
+		const bool master = true; // Only DNSSEC changesets are applied directly.
+		int ret = xfrin_apply_changeset(contents, set, master);
 		if (ret != KNOT_EOK) {
 			return ret;
 		}
@@ -1234,10 +1219,9 @@ int xfrin_apply_changesets(zone_t *zone,
 	dbg_xfrin_verb("Old contents apex: %p, new apex: %p\n",
 		       old_contents->apex, contents_copy->apex);
 	knot_changeset_t *set = NULL;
+	const bool master = (zone_master(zone) == NULL);
 	WALK_LIST(set, chsets->sets) {
-		ret = xfrin_apply_changeset(&set->old_data,
-		                            &set->new_data,
-		                            contents_copy, set);
+		ret = xfrin_apply_changeset(contents_copy, set, master);
 		if (ret != KNOT_EOK) {
 			xfrin_rollback_update(chsets, &contents_copy);
 			dbg_xfrin("Failed to apply changesets to zone: "
diff --git a/src/knot/zone/node.c b/src/knot/zone/node.c
index 5c0b3c256cb1b51894bc704d64f41fc79cd37dc9..c7a8db120cde0f5ec6e4a844aae9406cadedf210 100644
--- a/src/knot/zone/node.c
+++ b/src/knot/zone/node.c
@@ -164,7 +164,7 @@ zone_node_t *node_shallow_copy(const zone_node_t *src)
 	return dst;
 }
 
-int node_add_rrset(zone_node_t *node, const knot_rrset_t *rrset,  bool *ttl_err)
+int node_add_rrset(zone_node_t *node, const knot_rrset_t *rrset)
 {
 	if (node == NULL || rrset == NULL) {
 		return KNOT_EINVAL;
@@ -173,13 +173,14 @@ int node_add_rrset(zone_node_t *node, const knot_rrset_t *rrset,  bool *ttl_err)
 	for (uint16_t i = 0; i < node->rrset_count; ++i) {
 		if (node->rrs[i].type == rrset->type) {
 			struct rr_data *node_data = &node->rrs[i];
-			if (ttl_err) {
-				// Do TTL check.
-				*ttl_err = ttl_error(node_data, rrset);
+			const bool ttl_err = ttl_error(node_data, rrset);
+			int ret = knot_rdataset_merge(&node_data->rrs,
+			                              &rrset->rrs, NULL);
+			if (ret != KNOT_EOK) {
+				return ret;
+			} else {
+				return ttl_err ? KNOT_ETTL : KNOT_EOK;
 			}
-
-			return knot_rdataset_merge(&node_data->rrs,
-			                           &rrset->rrs, NULL);
 		}
 	}
 
diff --git a/src/knot/zone/node.h b/src/knot/zone/node.h
index 65e4ff22629cf80e782d8a76cb20ca23c70de857..e49f88048935e4a6f6ad59bf6aee1a945f670b95 100644
--- a/src/knot/zone/node.h
+++ b/src/knot/zone/node.h
@@ -126,12 +126,10 @@ zone_node_t *node_shallow_copy(const zone_node_t *src);
  *
  * \param node     Node to add the RRSet to.
  * \param rrset    RRSet to add.
- * \param ttl_err  Set to true if TTL error occured when inserting. New zone API
- *                 will obsolete this parameter.
  *
  * \return KNOT_E*
  */
-int node_add_rrset(zone_node_t *node, const knot_rrset_t *rrset, bool *ttl_err);
+int node_add_rrset(zone_node_t *node, const knot_rrset_t *rrset);
 
 /*!
  * \brief Removes data for given RR type from node.
diff --git a/src/knot/zone/zone-contents.c b/src/knot/zone/zone-contents.c
index 7129c1ea9adb10e067d2ba03342738b8fdd69c62..c6bf11249ceb0284ea3e28cf7acc5d911b7117f2 100644
--- a/src/knot/zone/zone-contents.c
+++ b/src/knot/zone/zone-contents.c
@@ -644,7 +644,7 @@ static zone_node_t *knot_zone_contents_get_nsec3_node(
 
 static int insert_rr(knot_zone_contents_t *z,
                      const knot_rrset_t *rr, zone_node_t **n,
-                     bool nsec3, bool *ttl_err)
+                     bool nsec3)
 {
 	if (z == NULL || knot_rrset_empty(rr) || n == NULL) {
 		return KNOT_EINVAL;
@@ -674,7 +674,7 @@ static int insert_rr(knot_zone_contents_t *z,
 		}
 	}
 
-	return node_add_rrset(*n, rr, ttl_err);
+	return node_add_rrset(*n, rr);
 }
 
 static int recreate_normal_tree(const knot_zone_contents_t *z,
@@ -779,9 +779,9 @@ static bool rrset_is_nsec3rel(const knot_rrset_t *rr)
 }
 
 int knot_zone_contents_add_rr(knot_zone_contents_t *z,
-                              const knot_rrset_t *rr, zone_node_t **n, bool *ttl_err)
+                              const knot_rrset_t *rr, zone_node_t **n)
 {
-	return insert_rr(z, rr, n, rrset_is_nsec3rel(rr), ttl_err);
+	return insert_rr(z, rr, n, rrset_is_nsec3rel(rr));
 }
 
 /*----------------------------------------------------------------------------*/
diff --git a/src/knot/zone/zone-contents.h b/src/knot/zone/zone-contents.h
index d8b1716bf5aef6841e0aa6b01514153a3380f44a..9eeac9a6b3cbf769308b985a4f3eab9a675fe715 100644
--- a/src/knot/zone/zone-contents.h
+++ b/src/knot/zone/zone-contents.h
@@ -93,7 +93,7 @@ void knot_zone_contents_set_gen_old(knot_zone_contents_t *contents);
 void knot_zone_contents_set_gen_new(knot_zone_contents_t *contents);
 
 int knot_zone_contents_add_rr(knot_zone_contents_t *z,
-                              const knot_rrset_t *rr, zone_node_t **n, bool *ttl_err);
+                              const knot_rrset_t *rr, zone_node_t **n);
 
 int knot_zone_contents_remove_node(knot_zone_contents_t *contents,
 	const knot_dname_t *owner);
diff --git a/src/knot/zone/zone-create.c b/src/knot/zone/zone-create.c
index 70f7d076e1a3dc4493bc613da56b0860d50c6163..0f928860179c382358366b0db6e259acb09c8eff 100644
--- a/src/knot/zone/zone-create.c
+++ b/src/knot/zone/zone-create.c
@@ -55,9 +55,8 @@ static int add_rdata_to_rr(knot_rrset_t *rrset, const zs_scanner_t *scanner)
 	                         scanner->r_ttl, NULL);
 }
 
-static bool handle_err(zcreator_t *zc,
-                       const knot_rrset_t *rr,
-                       int ret)
+static bool handle_err(zcreator_t *zc, const zone_node_t *node,
+                       const knot_rrset_t *rr, int ret, bool master)
 {
 	char *zname = zc->z ? knot_dname_to_str(zc->z->apex->owner) : NULL;
 	char *rrname = rr ? knot_dname_to_str(rr->owner) : NULL;
@@ -67,6 +66,13 @@ static bool handle_err(zcreator_t *zc,
 		free(zname);
 		free(rrname);
 		return true;
+	} else if (ret == KNOT_ETTL) {
+		free(zname);
+		free(rrname);
+		assert(node);
+		log_ttl_error(node, rr);
+		// Fail if we're the master for this zone.
+		return !master;
 	} else {
 		log_zone_error("Zone %s: Cannot process record %s, stopping.\n",
 		               zname ? zname : "unknown", rrname ? rrname : "unknown");
@@ -76,8 +82,7 @@ static bool handle_err(zcreator_t *zc,
 	}
 }
 
-static int log_ttl(const zcreator_t *zc, const zone_node_t *node,
-                   const knot_rrset_t *rr)
+void log_ttl_error(const zone_node_t *node, const knot_rrset_t *rr)
 {
 	err_handler_t err_handler;
 	err_handler_init(&err_handler);
@@ -91,16 +96,9 @@ static int log_ttl(const zcreator_t *zc, const zone_node_t *node,
 		*info_str = '\0';
 	}
 
-	if (zc->master) {
-		/*!< \todo REPLACE WITH FATAL ERROR */
-		err_handler_handle_error(&err_handler, node,
-		                         ZC_ERR_TTL_MISMATCH, info_str);
-		return KNOT_EMALF;
-	} else {
-		err_handler_handle_error(&err_handler, node,
-		                         ZC_ERR_TTL_MISMATCH, info_str);
-		return KNOT_EOK;
-	}
+	/*!< \todo REPLACE WITH FATAL ERROR for master. */
+	err_handler_handle_error(&err_handler, node,
+	                         ZC_ERR_TTL_MISMATCH, info_str);
 }
 
 int zcreator_step(zcreator_t *zc, const knot_rrset_t *rr)
@@ -115,25 +113,19 @@ int zcreator_step(zcreator_t *zc, const knot_rrset_t *rr)
 		return KNOT_EOK;
 	}
 
-	bool ttl_err = false;
 	zone_node_t *node = NULL;
-	int ret = knot_zone_contents_add_rr(zc->z, rr, &node, &ttl_err);
-	if (ret < 0) {
-		if (!handle_err(zc, rr, ret)) {
+	int ret = knot_zone_contents_add_rr(zc->z, rr, &node);
+	if (ret != KNOT_EOK) {
+		if (!handle_err(zc, node, rr, ret, zc->master)) {
 			// Fatal error
 			return ret;
 		}
-		// Recoverable error, skip record
-		return KNOT_EOK;
-	}
-	assert(node);
-
-	if (ttl_err) {
-		ret = log_ttl(zc, node, rr);
-		if (ret != KNOT_EOK) {
-			return ret;
+		if (ret == KNOT_EOUTOFZONE) {
+			// Skip out-of-zone record
+			return KNOT_EOK;
 		}
 	}
+	assert(node);
 
 	// Do node semantic checks
 	err_handler_t err_handler;
diff --git a/src/knot/zone/zone-create.h b/src/knot/zone/zone-create.h
index 8cb15d4ed69271c88a4007587e4fa547e1e8fd6e..92e26c3de113be409df4350904af295d7a539887 100644
--- a/src/knot/zone/zone-create.h
+++ b/src/knot/zone/zone-create.h
@@ -82,10 +82,30 @@ knot_zone_contents_t *zonefile_load(zloader_t *loader);
  */
 void zonefile_close(zloader_t *loader);
 
+/*!
+ * \brief Adds one RR into zone.
+ *
+ * \param zl  Zone loader.
+ * \param rr  RR to add.
+ *
+ * \return KNOT_E*
+ */
 int zcreator_step(zcreator_t *zl, const knot_rrset_t *rr);
 
+/*!
+ * \brief Scanner error processing function.
+ * \param scanner  Scanner to use.
+ */
 void process_error(zs_scanner_t *scanner);
 
+/*!
+ * \brief Logs TTL mismatch error.
+ *
+ * \param node    Node with TTL mismatch.
+ * \param rr      RR that caused the mismatch.
+ */
+void log_ttl_error(const zone_node_t *node, const knot_rrset_t *rr);
+
 #endif /* _KNOTD_ZONELOAD_H_ */
 
 /*! @} */
diff --git a/tests/process_query.c b/tests/process_query.c
index e94abd07d138298d59a187839f3e7c00046020ec..f0b4d76e968ea642fa679ef8208c1533783bc456 100644
--- a/tests/process_query.c
+++ b/tests/process_query.c
@@ -52,7 +52,7 @@ void create_root_zone(server_t *server, mm_ctx_t *mm)
 	                                         KNOT_RRTYPE_SOA, KNOT_CLASS_IN,
 	                                         NULL);
 	knot_rrset_add_rdata(soa_rrset, SOA_RDATA, SOA_RDLEN, 7200, NULL);
-	node_add_rrset(root->contents->apex, soa_rrset, NULL);
+	node_add_rrset(root->contents->apex, soa_rrset);
 
 	/* Bake the zone. */
 	zone_node_t *first_nsec3 = NULL, *last_nsec3 = NULL;