diff --git a/lib/cache/cdb_api.h b/lib/cache/cdb_api.h
index 9e8a34d58..b589b564a 100644
--- a/lib/cache/cdb_api.h
+++ b/lib/cache/cdb_api.h
@@ -65,11 +65,15 @@ struct kr_cdb_api {
 	 * \return error code - accepting RW transactions can fail with LMDB.
 	 */
 	int (*commit)(kr_cdb_pt db, struct kr_cdb_stats *stat, bool accept_rw, bool reset_ro);
+	/** Run before a row of operations to ensure they happen in a single RW transaction,
+	 *   at least in case of successes. */
+	int (*txn_open_rw)(kr_cdb_pt db, struct kr_cdb_stats *stat/*unused*/);
 
 	/* Data access */
 
 	int (*read)(kr_cdb_pt db, struct kr_cdb_stats *stat,
 			const knot_db_val_t *key, knot_db_val_t *val, int maxcount);
+	/* TODO: the behavior of write() around transactions is a bit complex. */
 	int (*write)(kr_cdb_pt db, struct kr_cdb_stats *stat, const knot_db_val_t *key,
 			knot_db_val_t *val, int maxcount);
 
@@ -111,13 +115,19 @@ struct kr_cdb_api {
 	int (*check_health)(kr_cdb_pt db, struct kr_cdb_stats *stat);
 
 
-	/** Start iterating; return the first *val with *key.
+	/** Start iterating: get the first *val with *key + return error code.
 	 *
-	 * This only makes sense if !is_cache.
-	 * TODO: it only works inside RO transactions for now.
+	 * - in cache: ensures a RO transaction (and commits the RW txn if any)
+	 * - in ruledb: transaction is preserved if exists, otherwise RO txn gets opened
 	 */
 	int (*it_first)(kr_cdb_pt db, struct kr_cdb_stats *stat,
 			const knot_db_val_t *key, knot_db_val_t *val);
-	/** Advance to the next *val with the same key. */
+	/** Advance to the next *val with the same key.  Return error code. */
 	int (*it_next)(kr_cdb_pt db, struct kr_cdb_stats *stat, knot_db_val_t *val);
+	/** Delete the current *val + return error code.
+	 *
+	 * You can it_next() to continue.
+	 * This assumed that you got it_first() in a RW txn.
+	 */
+	int (*it_del)(kr_cdb_pt db, struct kr_cdb_stats *stat);
 };
diff --git a/lib/cache/cdb_lmdb.c b/lib/cache/cdb_lmdb.c
index 0319b5cbb..6221c9492 100644
--- a/lib/cache/cdb_lmdb.c
+++ b/lib/cache/cdb_lmdb.c
@@ -38,12 +38,13 @@ struct lmdb_env
 	 *
 	 * - only one of (ro,rw) may be active at once
 	 * - non-NULL .ro may be active or reset
-	 * - non-NULL .rw is always active
+	 * - ro_curs can survive an RW transaction and renewed later in an RO txn
+	 * - non-NULL .rw is always active, non-NULL rw_curs is always active
 	 */
 	struct {
 		bool ro_active, ro_curs_active;
 		MDB_txn *ro, *rw;
-		MDB_cursor *ro_curs;
+		MDB_cursor *ro_curs, *rw_curs;
 	} txn;
 
 	bool is_cache; /**< cache vs. rules; from struct kr_cdb_opts::is_cache */
@@ -236,6 +237,7 @@ static int cdb_commit(kr_cdb_pt db, struct kr_cdb_stats *stats, bool accept_rw,
 			mdb_txn_abort(env->txn.rw);
 		}
 		env->txn.rw = NULL; /* the transaction got freed even in case of errors */
+		env->txn.rw_curs = NULL; /* auto-closed with txn if existed */
 	} else if (reset_ro && env->txn.ro && env->txn.ro_active) {
 		mdb_txn_reset(env->txn.ro);
 		env->txn.ro_active = false;
@@ -244,21 +246,39 @@ static int cdb_commit(kr_cdb_pt db, struct kr_cdb_stats *stats, bool accept_rw,
 	return ret;
 }
 
-/** Obtain a read-only cursor (and a read-only transaction).
- * TODO: allow RW transaction (for ruledb iterator) */
+static int cdb_txn_open_rw(kr_cdb_pt db, struct kr_cdb_stats *stat/*unused*/)
+{
+	struct lmdb_env *env = db2env(db);
+	MDB_txn *txn = NULL;
+	return txn_get(env, &txn, false);
+}
+
+/** Obtain a cursor (and a transaction).
+ *
+ * - for cache these are read only
+ * - for policy the RW transaction gets preserved if open */
 static int txn_curs_get(struct lmdb_env *env, MDB_cursor **curs, struct kr_cdb_stats *stats)
 {
 	if (kr_fails_assert(env && curs))
 		return kr_error(EINVAL);
 	if (env->txn.ro_curs_active)
-		goto success;
-	/* Only in a read-only txn; TODO: it's a bit messy/coupled
-	 * At least for rules we don't do the auto-commit feature. */
-	if (env->txn.rw) {
-		if (!env->is_cache) return kr_error(EINPROGRESS);
+		goto success_ro;
+	/* Only in a read-only txn if for cache; TODO: it's a bit messy/coupled */
+	if (env->txn.rw && env->is_cache) {
 		int ret = cdb_commit(env2db(env), stats, true, false);
 		if (ret) return ret;
 	}
+	if (env->txn.rw && !env->is_cache) {
+		if (!env->txn.rw_curs) {
+			MDB_txn *txn = env->txn.rw;
+			int ret = mdb_cursor_open(txn, env->dbi, &env->txn.rw_curs);
+			if (ret) return lmdb_error(env, ret);
+		}
+		if (kr_fails_assert(env->txn.rw_curs))
+			return kr_error(EINVAL);
+		*curs = env->txn.rw_curs;
+		return kr_ok();
+	}
 	MDB_txn *txn = NULL;
 	int ret = txn_get(env, &txn, true);
 	if (ret) return ret;
@@ -270,7 +290,7 @@ static int txn_curs_get(struct lmdb_env *env, MDB_cursor **curs, struct kr_cdb_s
 	}
 	if (ret) return lmdb_error(env, ret);
 	env->txn.ro_curs_active = true;
-success:
+success_ro:
 	kr_assert(env->txn.ro_curs_active && env->txn.ro && env->txn.ro_active
 			 && !env->txn.rw);
 	*curs = env->txn.ro_curs;
@@ -301,6 +321,7 @@ static void txn_abort(struct lmdb_env *env)
 	if (env->txn.rw) {
 		mdb_txn_abort(env->txn.rw);
 		env->txn.rw = NULL; /* the transaction got freed even in case of errors */
+		env->txn.rw_curs = NULL; /* auto-closed with txn if existed */
 	}
 }
 
@@ -940,7 +961,7 @@ static int cdb_it_next(kr_cdb_pt db, struct kr_cdb_stats *stats, knot_db_val_t *
 	if (kr_fails_assert(db && val))
 		return kr_error(EINVAL);
 	struct lmdb_env *env = db2env(db);
-	if (kr_fails_assert(!env->is_cache && env->txn.ro_curs_active))
+	if (kr_fails_assert(!env->is_cache && (env->txn.ro_curs_active || env->txn.rw_curs)))
 		return kr_error(EINVAL);
 
 	MDB_cursor *curs = NULL;
@@ -952,19 +973,35 @@ static int cdb_it_next(kr_cdb_pt db, struct kr_cdb_stats *stats, knot_db_val_t *
 	*val = val_mdb2knot(val2_m);
 	return kr_ok();
 }
+static int cdb_it_del(kr_cdb_pt db, struct kr_cdb_stats *stats)
+{
+	if (kr_fails_assert(db))
+		return kr_error(EINVAL);
+	struct lmdb_env *env = db2env(db);
+	if (kr_fails_assert(!env->is_cache && env->txn.rw_curs))
+		return kr_error(EINVAL);
+
+	MDB_cursor *curs = NULL;
+	int ret = txn_curs_get(env, &curs, stats);
+	if (ret) return ret;
+	ret = mdb_cursor_del(curs, 0);
+	if (ret) return lmdb_error(env, ret);
+	return kr_ok();
+}
 
 
 const struct kr_cdb_api *kr_cdb_lmdb(void)
 {
 	static const struct kr_cdb_api api = {
 		"lmdb",
-		cdb_init, cdb_deinit, cdb_count, cdb_clear, cdb_commit,
+		cdb_init, cdb_deinit, cdb_count, cdb_clear,
+		cdb_commit, cdb_txn_open_rw,
 		cdb_readv, cdb_writev, cdb_remove,
 		cdb_match,
 		cdb_read_leq, cdb_read_less,
 		cdb_usage_percent, cdb_get_maxsize,
 		cdb_check_health,
-		cdb_it_first, cdb_it_next,
+		cdb_it_first, cdb_it_next, cdb_it_del,
 	};
 	return &api;
 }
diff --git a/lib/rules/api.c b/lib/rules/api.c
index d9dbfcf1f..4c213d6f5 100644
--- a/lib/rules/api.c
+++ b/lib/rules/api.c
@@ -663,21 +663,27 @@ int kr_rule_local_data_merge(const knot_rrset_t *rrs, const kr_rule_tags_t tags,
 	uint8_t key_data[KEY_MAXLEN];
 	knot_db_val_t key = local_data_key(rrs, key_data, RULESET_DEFAULT);
 	knot_db_val_t val;
+	int ret = ruledb_op(txn_open_rw);
+	if (ret)
+		return kr_error(ret);
 	// Transaction: we assume that we're in a RW transaction already,
 	// so that here we already "have a lock" on the last version.
-	// FIXME: iterate over multiple tags, once iterator supports RW TXN
-	int ret = ruledb_op(read, &key, &val, 1);
+	// Multiple variants are possible, with different tags.
+	for (ret = ruledb_op(it_first, &key, &val); ret == 0; ret = ruledb_op(it_next, &val)) {
+		// we're looking for the same tag-set
+		kr_rule_tags_t tags_old;
+		if (deserialize_fails_assert(&val, &tags_old) || tags_old != tags)
+			continue;
+		kr_rule_opts_t opts_old;
+		if (deserialize_fails_assert(&val, &opts_old))
+			continue;
+		break;
+	}
 	if (abs(ret) == abs(ENOENT))
 		goto fallback;
 	if (ret)
 		return kr_error(ret);
-	// check tags
-	kr_rule_tags_t tags_old;
-	if (deserialize_fails_assert(&val, &tags_old) || tags_old != tags)
-		goto fallback;
-	kr_rule_opts_t opts_old;
-	if (deserialize_fails_assert(&val, &opts_old))
-		goto fallback;
+
 	// merge TTLs
 	uint32_t ttl;
 	if (deserialize_fails_assert(&val, &ttl))
@@ -700,6 +706,10 @@ int kr_rule_local_data_merge(const knot_rrset_t *rrs, const kr_rule_tags_t tags,
 		mm_ctx_delete(mm);
 		return kr_error(ret);
 	}
+	// ATM ruledb does not overwrite, so we `remove` before `write`.
+	ret = ruledb_op(it_del);
+	if (ret)
+		return kr_error(ret);
 	// everything is ready to insert the merged RRset
 	ret = local_data_ins(key, &rrs_new, NULL, tags, opts);
 	mm_ctx_delete(mm);
diff --git a/lib/rules/api.h b/lib/rules/api.h
index feea98e09..c3af5901b 100644
--- a/lib/rules/api.h
+++ b/lib/rules/api.h
@@ -144,10 +144,8 @@ const uint32_t KR_RULE_TTL_DEFAULT;
 KR_EXPORT
 int kr_rule_local_data_ins(const knot_rrset_t *rrs, const knot_rdataset_t *sig_rds,
 				kr_rule_tags_t tags, kr_rule_opts_t opts);
-/** Merge RRs into a local data rule.
+/** Merge RRs into a local data rule with the same set of tags.
  *
- * - FIXME: with multiple tags variants for the same name-type pair,
- *     you typically end up with a single RR per RRset
  * - RRSIGs get dropped, if any were attached.
  * - We assume that this is called with a RW transaction open already,
  *   which is always true in normal usage (long RW txn covering whole config).
