diff --git a/src/knot/ctl/commands.c b/src/knot/ctl/commands.c
index cb27333f09d191677493a99f952efe888da1b5ea..ce8d943b9ad4ebfcec32b0e8e47db1d6fc14a9c3 100644
--- a/src/knot/ctl/commands.c
+++ b/src/knot/ctl/commands.c
@@ -30,6 +30,7 @@
 #include "knot/nameserver/query_module.h"
 #include "knot/updates/zone-update.h"
 #include "knot/zone/backup.h"
+#include "knot/zone/digest.h"
 #include "knot/zone/timers.h"
 #include "knot/zone/zonedb-load.h"
 #include "knot/zone/zonefile.h"
@@ -694,17 +695,26 @@ static int zone_txn_commit(zone_t *zone, _unused_ ctl_args_t *args)
 	// Sign update.
 	conf_val_t val = conf_zone_get(conf(), C_DNSSEC_SIGNING, zone->name);
 	bool dnssec_enable = conf_bool(&val);
+	val = conf_zone_get(conf(), C_ZONEMD_GENERATE, zone->name);
+	unsigned digest_alg = conf_opt(&val);
 	if (dnssec_enable) {
 		zone_sign_reschedule_t resch = { 0 };
 		bool full = (zone->control_update->flags & UPDATE_FULL);
 		zone_sign_roll_flags_t rflags = KEY_ROLL_ALLOW_ALL;
 		ret = (full ? knot_dnssec_zone_sign(zone->control_update, conf(), 0, rflags, 0, &resch) :
 		              knot_dnssec_sign_update(zone->control_update, conf(), &resch));
-		if (ret != KNOT_EOK) {
-			zone_control_clear(zone);
-			return ret;
-		}
 		event_dnssec_reschedule(conf(), zone, &resch, false);
+	} else if (digest_alg != ZONE_DIGEST_NONE) {
+		if (zone_update_to(zone->control_update) == NULL) {
+			ret = zone_update_increment_soa(zone->control_update, conf());
+		}
+		if (ret == KNOT_EOK) {
+			ret = zone_update_add_digest(zone->control_update, digest_alg, false);
+		}
+	}
+	if (ret != KNOT_EOK) {
+		zone_control_clear(zone);
+		return ret;
 	}
 
 	ret = zone_update_commit(conf(), zone->control_update);
diff --git a/src/knot/events/handlers/load.c b/src/knot/events/handlers/load.c
index ba69008bcbf6506c61398b0adabb64e9023eeedc..93f10b5df6cca2d5d3242b82422a9a52a0897a04 100644
--- a/src/knot/events/handlers/load.c
+++ b/src/knot/events/handlers/load.c
@@ -23,6 +23,7 @@
 #include "knot/dnssec/zone-events.h"
 #include "knot/events/handlers.h"
 #include "knot/events/replan.h"
+#include "knot/zone/digest.h"
 #include "knot/zone/serial.h"
 #include "knot/zone/zone-diff.h"
 #include "knot/zone/zone-load.h"
@@ -272,6 +273,9 @@ load_end:
 		}
 	}
 
+	val = conf_zone_get(conf, C_ZONEMD_GENERATE, zone->name);
+	unsigned digest_alg = conf_opt(&val);
+
 	// Sign zone using DNSSEC if configured.
 	zone_sign_reschedule_t dnssec_refresh = { 0 };
 	if (dnssec_enable) {
@@ -285,6 +289,16 @@ load_end:
 			                 "'zonefile-load: difference' should be set to avoid malformed "
 			                 "IXFR after manual zone file update");
 		}
+	} else if (digest_alg != ZONE_DIGEST_NONE) {
+		if (zone_update_to(&up) == NULL || middle_serial == zone->zonefile.serial) {
+			ret = zone_update_increment_soa(&up, conf);
+		}
+		if (ret == KNOT_EOK) {
+			ret = zone_update_add_digest(&up, digest_alg, false);
+		}
+		if (ret != KNOT_EOK) {
+			goto cleanup;
+		}
 	}
 
 	// If the change is only automatically incremented SOA serial, make it no change.
diff --git a/src/knot/events/handlers/refresh.c b/src/knot/events/handlers/refresh.c
index 88c8daf270efac8719a6198c9448747e11608770..80f01bd0269d4edd6f502e9cf4cfa6e96a87e8e1 100644
--- a/src/knot/events/handlers/refresh.c
+++ b/src/knot/events/handlers/refresh.c
@@ -30,6 +30,7 @@
 #include "knot/query/requestor.h"
 #include "knot/updates/changesets.h"
 #include "knot/zone/adjust.h"
+#include "knot/zone/digest.h"
 #include "knot/zone/serial.h"
 #include "knot/zone/zone.h"
 #include "knot/zone/zonefile.h"
@@ -256,14 +257,20 @@ static int axfr_finalize(struct refresh_data *data)
 		return ret;
 	}
 
+	val = conf_zone_get(data->conf, C_ZONEMD_GENERATE, data->zone->name);
+	unsigned digest_alg = conf_opt(&val);
+
 	if (dnssec_enable) {
 		zone_sign_reschedule_t resch = { 0 };
 		ret = knot_dnssec_zone_sign(&up, data->conf, ZONE_SIGN_KEEP_SERIAL, KEY_ROLL_ALLOW_ALL, 0, &resch);
-		if (ret != KNOT_EOK) {
-			zone_update_clear(&up);
-			return ret;
-		}
 		event_dnssec_reschedule(data->conf, data->zone, &resch, true);
+	} else if (digest_alg != ZONE_DIGEST_NONE) {
+		assert(zone_update_to(&up) != NULL);
+		ret = zone_update_add_digest(&up, digest_alg, false);
+	}
+	if (ret != KNOT_EOK) {
+		zone_update_clear(&up);
+		return ret;
 	}
 
 	ret = zone_update_commit(data->conf, &up);
@@ -516,14 +523,20 @@ static int ixfr_finalize(struct refresh_data *data)
 		return ret;
 	}
 
+	val = conf_zone_get(data->conf, C_ZONEMD_GENERATE, data->zone->name);
+	unsigned digest_alg = conf_opt(&val);
+
 	if (dnssec_enable) {
 		zone_sign_reschedule_t resch = { 0 };
 		ret = knot_dnssec_sign_update(&up, data->conf, &resch);
-		if (ret != KNOT_EOK) {
-			zone_update_clear(&up);
-			return ret;
-		}
 		event_dnssec_reschedule(data->conf, data->zone, &resch, true);
+	} else if (digest_alg != ZONE_DIGEST_NONE) {
+		assert(zone_update_to(&up) != NULL);
+		ret = zone_update_add_digest(&up, digest_alg, false);
+	}
+	if (ret != KNOT_EOK) {
+		zone_update_clear(&up);
+		return ret;
 	}
 
 	ret = zone_update_commit(data->conf, &up);
diff --git a/src/knot/events/handlers/update.c b/src/knot/events/handlers/update.c
index 43e130060f7ca2f2e782307b938c4d362298c698..adad434beba1b80417c8cdf7d0edd9bbad56b359 100644
--- a/src/knot/events/handlers/update.c
+++ b/src/knot/events/handlers/update.c
@@ -22,6 +22,7 @@
 #include "knot/query/capture.h"
 #include "knot/query/requestor.h"
 #include "knot/updates/ddns.h"
+#include "knot/zone/digest.h"
 #include "knot/zone/zone.h"
 #include "libdnssec/random.h"
 #include "libknot/libknot.h"
@@ -157,15 +158,24 @@ static int process_normal(conf_t *conf, zone_t *zone, list_t *requests)
 	// Sign update.
 	conf_val_t val = conf_zone_get(conf, C_DNSSEC_SIGNING, zone->name);
 	bool dnssec_enable = conf_bool(&val);
+	val = conf_zone_get(conf, C_ZONEMD_GENERATE, zone->name);
+	unsigned digest_alg = conf_opt(&val);
 	if (dnssec_enable) {
 		zone_sign_reschedule_t resch = { 0 };
 		ret = knot_dnssec_sign_update(&up, conf, &resch);
-		if (ret != KNOT_EOK) {
-			zone_update_clear(&up);
-			set_rcodes(requests, KNOT_RCODE_SERVFAIL);
-			return ret;
-		}
 		event_dnssec_reschedule(conf, zone, &resch, false); // false since we handle NOTIFY after processing ddns queue
+	} else if (digest_alg != ZONE_DIGEST_NONE) {
+		if (zone_update_to(&up) == NULL) {
+			ret = zone_update_increment_soa(&up, conf);
+		}
+		if (ret == KNOT_EOK) {
+			ret = zone_update_add_digest(&up, digest_alg, false);
+		}
+	}
+	if (ret != KNOT_EOK) {
+		zone_update_clear(&up);
+		set_rcodes(requests, KNOT_RCODE_SERVFAIL);
+		return ret;
 	}
 
 	// Apply changes.
diff --git a/src/knot/updates/zone-update.c b/src/knot/updates/zone-update.c
index 0529dec5fa5a86d045c953e5a83c9be96f57cc31..25eb2579d8545484a97d2a765e4667bdb930d2ff 100644
--- a/src/knot/updates/zone-update.c
+++ b/src/knot/updates/zone-update.c
@@ -882,24 +882,6 @@ int zone_update_commit(conf_t *conf, zone_update_t *update)
 
 	conf_val_t val = conf_zone_get(conf, C_DNSSEC_SIGNING, update->zone->name);
 	bool dnssec = conf_bool(&val);
-	val = conf_zone_get(conf, C_ZONEMD_GENERATE, update->zone->name);
-	unsigned digest_alg = conf_opt(&val);
-	bool do_digest = (digest_alg != ZONE_DIGEST_NONE && !dnssec); // in case of DNSSEC, digest is part of signing routine
-	if (do_digest && !(update->flags & UPDATE_FULL) && zone_update_to(update) == NULL) {
-		// cold start, decide if (digest & bump SOA) or NOOP
-		// yes, computing hash twice, but in rare situation: cold start & exists & invalid
-		if (zone_contents_digest_exists(update->new_cont, digest_alg, false)) {
-			do_digest = false;
-		} else {
-			ret = zone_update_increment_soa(update, conf);
-		}
-	}
-	if (do_digest && ret == KNOT_EOK) {
-		ret = zone_update_add_digest(update, digest_alg, false);
-	}
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
 
 	conf_val_t thr = conf_zone_get(conf, C_ADJUST_THR, update->zone->name);
 	if ((update->flags & (UPDATE_HYBRID | UPDATE_FULL))) {
diff --git a/tests-extra/tests/zone/zonemd_flush/test.py b/tests-extra/tests/zone/zonemd_flush/test.py
index 7af8d7ef4bb2b599dcee67c54c07636191e2675d..f1a1e3a5602bf85150188dfdfb9ac35d800eec00 100644
--- a/tests-extra/tests/zone/zonemd_flush/test.py
+++ b/tests-extra/tests/zone/zonemd_flush/test.py
@@ -2,9 +2,13 @@
 
 '''Flushing the zone after ZONEMD generation.'''
 
+import random
+
 from dnstest.test import Test
 from dnstest.utils import *
 
+t = Test()
+
 def has_zonemd(server, zone, alg):
     zfn = server.zones[zone.name].zfile.path
     with open(zfn) as zf:
@@ -15,31 +19,82 @@ def has_zonemd(server, zone, alg):
     return False
 
 def check_zonemd(server, zone, alg):
+    t.sleep(2)
     for z in zone:
         if not has_zonemd(server, z, alg):
             set_err("NO ZONEMD in %s" % z.name)
 
-t = Test()
+def del_zonemd1(server, zone):
+   zf = server.zones[zone.name].zfile
+   zf.update_soa()
+
+   with open(zf.path, "r+") as f:
+       lines = f.readlines()
+       f.seek(0)
+       for line in lines:
+           if "ZONEMD" not in line:
+               f.write(line)
+       f.truncate()
+
+def del_zonemd(server, zone):
+    for z in zone:
+        del_zonemd1(server, z)
+
+# NOTE parameter "serials" is updated
+def check_serial_incr(server, zones, serials, expect_incr, msg):
+    new_serials = server.zones_wait(zones, serials)
+    for z in zones:
+        if new_serials[z.name] != serials[z.name] + expect_incr:
+            err_str = "%s: zone %s serial incremented by %d" % (msg, z.name, new_serials[z.name] - serial[z.name]);
+            detail_log(err_str)
+            set_err(err_str)
+        serials[z.name] = new_serials[z.name]
 
 master = t.server("knot")
+slave = t.server("knot")
 
 zone = t.zone_rnd(2, dnssec=False, records=10)
-t.link(zone, master)
+t.link(zone, master, slave, ixfr=random.choice([True, False]))
 
 master.zonefile_sync = 0
 master.zonemd_generate = "zonemd-sha384"
+slave.zonemd_verify = True
 
 t.start()
 
-serial = master.zones_wait(zone)
-t.sleep(4)
+serial = slave.zones_wait(zone)
 check_zonemd(master, zone, "1")
 
 master.zonemd_generate = "zonemd-sha512"
 master.gen_confile()
 master.reload()
-master.zones_wait(zone, serial)
-t.sleep(4)
+check_serial_incr(slave, zone, serial, 1, "alg change")
+check_zonemd(master, zone, "2")
+
+del_zonemd(master, zone)
+master.ctl("zone-reload")
+check_serial_incr(slave, zone, serial, 2, "ZONEMD removed")
+check_zonemd(master, zone, "2")
+
+for z in zone:
+    master.random_ddns(z, allow_empty=False)
+check_serial_incr(slave, zone, serial, 1, "DDNS")
+
+for z in zone:
+    # BUMP SOA serial by 3 thru DDNS
+    resp = master.dig(z.name, "SOA")
+    soa = resp.resp.answer[0].to_rdataset()[0].to_text()
+    fields = soa.split()
+    fields[2] = str(int(fields[2]) + 3)
+    up = master.update(z)
+    up.add(z.name, 3600, "SOA", ' '.join(fields))
+    up.send("NOERROR")
+check_serial_incr(slave, zone, serial, 3, "SOA DDNS")
+
+for z in zone:
+    master.zones[z.name].zfile.update_rnd()
+master.ctl("zone-reload")
+check_serial_incr(slave, zone, serial, 2, "ZF reload")
 check_zonemd(master, zone, "2")
 
 t.end()