diff --git a/src/knot/modules/rrl/functions.c b/src/knot/modules/rrl/functions.c
index fcee6c3ae5443378a4be6c4fbfe87ea68e1fdfdf..01d89cb66949eedc8486a284eb2a6c62592b1887 100644
--- a/src/knot/modules/rrl/functions.c
+++ b/src/knot/modules/rrl/functions.c
@@ -19,14 +19,13 @@
 
 #include "knot/modules/rrl/functions.h"
 #include "knot/modules/rrl/kru.h"
+#include "contrib/macros.h"
 #include "contrib/musl/inet_ntop.h"
 #include "contrib/sockaddr.h"
 #include "contrib/time.h"
 #include "libdnssec/random.h"
 
-/* CIDR block prefix lengths for v4/v6 */
-// Hardcoded also in unit tests.
-
+// CIDR block prefix lengths for v4/v6 (hardcoded also in unit tests).
 #define RRL_V4_PREFIXES  (uint8_t[])       {  18,  20, 24, 32 }
 #define RRL_V4_RATE_MULT (kru_price_t[])   { 768, 256, 32,  1 }
 
@@ -41,10 +40,13 @@
 #define CLOCK_MONOTONIC_COARSE CLOCK_MONOTONIC
 #endif
 
+#define RRL_LIMIT_KOEF 1/2 // Avoid probabilistic rounding wherever possible.
+
 struct rrl_table {
 	kru_price_t v4_prices[RRL_V4_PREFIXES_CNT];
 	kru_price_t v6_prices[RRL_V6_PREFIXES_CNT];
 	uint32_t log_period;
+	bool rw_mode;
 	_Atomic uint32_t log_time;
 	_Alignas(64) uint8_t kru[];
 };
@@ -66,7 +68,8 @@ static void addr_tostr(char *dst, size_t maxlen, const struct sockaddr_storage *
 	}
 }
 
-static void rrl_log_limited(knotd_mod_t *mod, const struct sockaddr_storage *ss, const uint8_t prefix)
+static void rrl_log_limited(knotd_mod_t *mod, const struct sockaddr_storage *ss,
+                            const uint8_t prefix, bool rate)
 {
 	if (mod == NULL) {
 		return;
@@ -75,10 +78,12 @@ static void rrl_log_limited(knotd_mod_t *mod, const struct sockaddr_storage *ss,
 	char addr_str[SOCKADDR_STRLEN];
 	addr_tostr(addr_str, sizeof(addr_str), ss);
 
-	knotd_mod_log(mod, LOG_NOTICE, "address %s limited on /%d", addr_str, prefix);
+	knotd_mod_log(mod, LOG_NOTICE, "address %s limited on /%d by %s",
+	              addr_str, prefix, rate ? "rate" : "time");
 }
 
-rrl_table_t *rrl_create(size_t size, uint32_t instant_limit, uint32_t rate_limit, uint32_t log_period)
+rrl_table_t *rrl_create(size_t size, uint32_t instant_limit, uint32_t rate_limit,
+                        bool rw_mode, uint32_t log_period)
 {
 	if (size == 0 || instant_limit == 0 || rate_limit == 0) {
 		return NULL;
@@ -95,9 +100,12 @@ rrl_table_t *rrl_create(size_t size, uint32_t instant_limit, uint32_t rate_limit
 	}
 	memset(rrl, 0, rrl_size);
 
-	const kru_price_t base_price = KRU_LIMIT / instant_limit;
-	const kru_price_t max_decay = rate_limit > 1000ll * instant_limit ? base_price :
-		(uint64_t) base_price * rate_limit / 1000;
+	assert(rate_limit <= 1000ll * instant_limit); // Ensured by config check.
+	kru_price_t base_price = KRU_LIMIT / instant_limit;
+	const kru_price_t max_decay = (uint64_t)base_price * rate_limit / 1000;
+	if (!rw_mode) {
+		base_price = base_price * RRL_LIMIT_KOEF;
+	}
 
 	if (!KRU.initialize((struct kru *)rrl->kru, capacity_log, max_decay)) {
 		free(rrl);
@@ -112,9 +120,10 @@ rrl_table_t *rrl_create(size_t size, uint32_t instant_limit, uint32_t rate_limit
 		rrl->v6_prices[i] = base_price / RRL_V6_RATE_MULT[i];
 	}
 
+	rrl->rw_mode = rw_mode;
 	rrl->log_period = log_period;
 
-	struct timespec now_ts = {0};
+	struct timespec now_ts;
 	clock_gettime(CLOCK_MONOTONIC_COARSE, &now_ts);
 	uint32_t now = now_ts.tv_sec * 1000 + now_ts.tv_nsec / 1000000;
 	rrl->log_time = now - log_period;
@@ -124,42 +133,104 @@ rrl_table_t *rrl_create(size_t size, uint32_t instant_limit, uint32_t rate_limit
 
 int rrl_query(rrl_table_t *rrl, const struct sockaddr_storage *remote, knotd_mod_t *mod)
 {
-	if (rrl == NULL || remote == NULL) {
-		return KNOT_EINVAL;
-	}
+	assert(rrl);
+	assert(remote);
 
-	struct timespec now_ts = {0};
+	struct timespec now_ts;
 	clock_gettime(CLOCK_MONOTONIC_COARSE, &now_ts);
 	uint32_t now = now_ts.tv_sec * 1000 + now_ts.tv_nsec / 1000000;
 
-	uint8_t limited_prefix;
+	uint16_t load = 0;
+	uint8_t prefix = 0;
 	_Alignas(16) uint8_t key[16] = { 0 };
 	if (remote->ss_family == AF_INET6) {
 		struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)remote;
 		memcpy(key, &ipv6->sin6_addr, 16);
 
-		limited_prefix = KRU.limited_multi_prefix_or((struct kru *)rrl->kru, now,
-				1, key, RRL_V6_PREFIXES, rrl->v6_prices, RRL_V6_PREFIXES_CNT, NULL);
+		if (rrl->rw_mode) {
+			prefix = KRU.limited_multi_prefix_or(
+				(struct kru *)rrl->kru, now, 1, key, RRL_V6_PREFIXES,
+				rrl->v6_prices, RRL_V6_PREFIXES_CNT, NULL);
+		} else {
+			load = KRU.load_multi_prefix_max(
+				(struct kru *)rrl->kru, now, 1, key, RRL_V6_PREFIXES,
+				NULL, RRL_V6_PREFIXES_CNT, &prefix);
+		}
 	} else {
 		struct sockaddr_in *ipv4 = (struct sockaddr_in *)remote;
 		memcpy(key, &ipv4->sin_addr, 4);
 
-		limited_prefix = KRU.limited_multi_prefix_or((struct kru *)rrl->kru, now,
-				0, key, RRL_V4_PREFIXES, rrl->v4_prices, RRL_V4_PREFIXES_CNT, NULL);
+		if (rrl->rw_mode) {
+			prefix = KRU.limited_multi_prefix_or(
+				(struct kru *)rrl->kru, now, 0, key, RRL_V4_PREFIXES,
+				rrl->v4_prices, RRL_V4_PREFIXES_CNT, NULL);
+		} else {
+			load = KRU.load_multi_prefix_max(
+				(struct kru *)rrl->kru, now, 0, key, RRL_V4_PREFIXES,
+				NULL, RRL_V4_PREFIXES_CNT, &prefix);
+		}
+	}
+
+	if (rrl->rw_mode) {
+		if (prefix == 0) {
+			return KNOT_EOK;
+		}
+	} else {
+		if (load <= (1 << 16) * RRL_LIMIT_KOEF) {
+			return KNOT_EOK;
+		}
 	}
 
 	uint32_t log_time_orig = atomic_load_explicit(&rrl->log_time, memory_order_relaxed);
-	if (rrl->log_period && limited_prefix && (now - log_time_orig + 1024 >= rrl->log_period + 1024)) {
+	if (rrl->log_period && (now - log_time_orig + 1024 >= rrl->log_period + 1024)) {
 		do {
 			if (atomic_compare_exchange_weak_explicit(&rrl->log_time, &log_time_orig, now,
 			                                          memory_order_relaxed, memory_order_relaxed)) {
-				rrl_log_limited(mod, remote, limited_prefix);
+				rrl_log_limited(mod, remote, prefix, rrl->rw_mode);
 				break;
 			}
 		} while (now - log_time_orig + 1024 >= rrl->log_period + 1024);
 	}
 
-	return limited_prefix ? KNOT_ELIMIT : KNOT_EOK;
+	return KNOT_ELIMIT;
+}
+
+void rrl_update(rrl_table_t *rrl, const struct sockaddr_storage *remote, size_t value)
+{
+	assert(rrl);
+	assert(remote);
+	assert(!rrl->rw_mode);
+
+	struct timespec now_ts;
+	clock_gettime(CLOCK_MONOTONIC_COARSE, &now_ts);
+	uint32_t now = now_ts.tv_sec * 1000 + now_ts.tv_nsec / 1000000;
+
+	_Alignas(16) uint8_t key[16] = { 0 };
+	if (remote->ss_family == AF_INET6) {
+		struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)remote;
+		memcpy(key, &ipv6->sin6_addr, 16);
+
+		kru_price_t prices[RRL_V6_PREFIXES_CNT];
+		for (size_t i = 0; i < RRL_V6_PREFIXES_CNT; i++) {
+			prices[i] = MIN(value * rrl->v6_prices[i], (kru_price_t)-1LL);
+		}
+
+		(void)KRU.load_multi_prefix_max((struct kru *)rrl->kru, now,
+		                                1, key, RRL_V6_PREFIXES, prices,
+		                                RRL_V6_PREFIXES_CNT, NULL);
+	} else {
+		struct sockaddr_in *ipv4 = (struct sockaddr_in *)remote;
+		memcpy(key, &ipv4->sin_addr, 4);
+
+		kru_price_t prices[RRL_V4_PREFIXES_CNT];
+		for (size_t i = 0; i < RRL_V4_PREFIXES_CNT; i++) {
+			prices[i] = MIN(value * rrl->v4_prices[i], (kru_price_t)-1LL);
+		}
+
+		(void)KRU.load_multi_prefix_max((struct kru *)rrl->kru, now,
+		                                0, key, RRL_V4_PREFIXES, prices,
+		                                RRL_V4_PREFIXES_CNT, NULL);
+	}
 }
 
 bool rrl_slip_roll(int n_slip)
diff --git a/src/knot/modules/rrl/functions.h b/src/knot/modules/rrl/functions.h
index 934df369cc2a968493f72fa7c05447a636ea8ca8..0941c8378f057651aaff91d5ee1dbd4f5be0aefb 100644
--- a/src/knot/modules/rrl/functions.h
+++ b/src/knot/modules/rrl/functions.h
@@ -27,17 +27,21 @@ typedef struct rrl_table rrl_table_t;
  * \brief Create a RRL table.
  *
  * \param size Fixed table size.
- * \param rate Rate (in pkts/sec).
- * \param instant_limit Instant limit (number).
+ * \param instant_limit Instant limit.
+ * \param rate_limit Rate limit.
+ * \param rw_mode If disabled, RW operation is divided into R and W operations.
  * \param log_period If nonzero, maximum logging period (in milliseconds).
  *
  * \return created table or NULL.
  */
-rrl_table_t *rrl_create(size_t size, uint32_t instant_limit, uint32_t rate_limit, uint32_t log_period);
+rrl_table_t *rrl_create(size_t size, uint32_t instant_limit, uint32_t rate_limit,
+                        bool rw_mode, uint32_t log_period);
 
 /*!
  * \brief Query the RRL table for accept or deny, when the rate limit is reached.
  *
+ * \note This function is common to both RW and non-RW modes!
+ *
  * \param rrl RRL table.
  * \param remote Source address.
  * \param mod Query module (needed for logging).
@@ -47,6 +51,17 @@ rrl_table_t *rrl_create(size_t size, uint32_t instant_limit, uint32_t rate_limit
  */
 int rrl_query(rrl_table_t *rrl, const struct sockaddr_storage *remote, knotd_mod_t *mod);
 
+/*!
+ * \brief Update the RRL table.
+ *
+ * \note This function is only for the non-RW mode!
+ *
+ * \param rrl RRL table.
+ * \param remote Source address.
+ * \param value Value with which the table is updated.
+ */
+void rrl_update(rrl_table_t *rrl, const struct sockaddr_storage *remote, size_t value);
+
 /*!
  * \brief Roll a dice whether answer slips or not.
  *
diff --git a/src/knot/modules/rrl/rrl.c b/src/knot/modules/rrl/rrl.c
index 8d2ad05bef2a929eb0f5efd828ab52d72c9eddcc..e90648585124e0cb6897f7e200de4ccac9554abd 100644
--- a/src/knot/modules/rrl/rrl.c
+++ b/src/knot/modules/rrl/rrl.c
@@ -118,7 +118,7 @@ int rrl_load(knotd_mod_t *mod)
 	uint32_t rate_limit = knotd_conf_mod(mod, MOD_RATE_LIMIT).single.integer;
 	size_t size = knotd_conf_mod(mod, MOD_TBL_SIZE).single.integer;
 	uint32_t log_period = knotd_conf_mod(mod, MOD_LOG_PERIOD).single.integer;
-	ctx->rrl = rrl_create(size, instant_limit, rate_limit, log_period);
+	ctx->rrl = rrl_create(size, instant_limit, rate_limit, true, log_period);
 	if (ctx->rrl == NULL) {
 		ctx_free(ctx);
 		return KNOT_ENOMEM;
diff --git a/tests/modules/test_rrl.c b/tests/modules/test_rrl.c
index d948d21b4f298dfa45cf54e553d955f3345b9830..448ff9acd44236d5eb23e04df1db70323074aa66 100644
--- a/tests/modules/test_rrl.c
+++ b/tests/modules/test_rrl.c
@@ -32,11 +32,6 @@ int fakeclock_gettime(clockid_t clockid, struct timespec *tp);
 #include "knot/modules/rrl/functions.c"
 #undef clock_gettime
 
-#define RRL_TABLE_SIZE     (1 << 20)
-#define RRL_INSTANT_LIMIT  (1 << 8)
-#define RRL_RATE_LIMIT     (1 << 17)
-#define RRL_BASE_PRICE     (KRU_LIMIT / RRL_INSTANT_LIMIT)
-
 #define RRL_THREADS 4
 //#define RRL_SYNC_WITH_REAL_TIME
 
@@ -57,14 +52,17 @@ static inline kru_price_t get_mult(uint8_t prefixes[], kru_price_t mults[], size
 	return 0;
 }
 
+// Macro correction depending on the table mode.
+int DIFF = 0;
+
 // Instant limits and rate limits per msec.
-#define INST(Vx, prefix)  LIMIT(INSTANT, Vx, prefix)
-#define RATEM(Vx, prefix) (LIMIT(RATE, Vx, prefix) / 1000)
+#define INST(Vx, prefix)  (LIMIT(INSTANT, Vx, prefix) + DIFF)
+#define RATEM(Vx, prefix) (LIMIT(RATE, Vx, prefix) / 1000 + DIFF)
 
 // Expected range of limits for parallel test.
-#define RANGE_INST(Vx, prefix)   INST(Vx, prefix) - 1,   INST(Vx, prefix) + RRL_THREADS - 1
-#define RANGE_RATEM(Vx, prefix)  RATEM(Vx, prefix) - 1,  RATEM(Vx, prefix)
-#define RANGE_UNLIM(queries)     queries,                queries
+#define RANGE_INST(Vx, prefix)   INST(Vx, prefix) - 1,         INST(Vx, prefix) + RRL_THREADS - 1
+#define RANGE_RATEM(Vx, prefix)  RATEM(Vx, prefix) - 1 - DIFF, RATEM(Vx, prefix) + RRL_THREADS - DIFF
+#define RANGE_UNLIM(queries)     queries,                      queries
 
 /* Fix seed for randomness in RLL module. Change if improbable collisions arise. (one byte) */
 #define RRL_SEED_GENERIC  1
@@ -172,6 +170,9 @@ static void *rrl_runnable(void *arg)
 
 				if (rrl_query(d->rrl, &addr, NULL) == KNOT_EOK) {
 					atomic_fetch_add(&d->stages[si].hosts[hi].passed, 1);
+					if (!d->rrl->rw_mode) {
+						rrl_update(d->rrl, &addr, 1);
+					}
 				}
 
 			} while ((qi2 = (qi2 + d->prime) % (1 << BATCH_QUERIES_LOG)));
@@ -200,6 +201,9 @@ void count_test(char *desc, int expected_passing, double margin_fract,
 			cnt = i;
 			break;
 		}
+		if (!rrl->rw_mode) {
+			rrl_update(rrl, &addr, 1);
+		}
 	}
 
 	if (expected_passing < 0) expected_passing = -1;
@@ -213,12 +217,20 @@ void count_test(char *desc, int expected_passing, double margin_fract,
 	}
 }
 
-void test_rrl(void)
+void test_rrl(bool rw_mode)
 {
+	size_t RRL_TABLE_SIZE = (1 << 20);
+	uint32_t RRL_INSTANT_LIMIT = (1 << 7);
+	uint32_t RRL_RATE_LIMIT = (1 << 16);
+	if (rw_mode) {
+		RRL_INSTANT_LIMIT = (1 << 8);
+		RRL_RATE_LIMIT = (1 << 17);
+	}
+
 	fakeclock_init();
 
 	/* create rrl table */
-	rrl = rrl_create(RRL_TABLE_SIZE, RRL_INSTANT_LIMIT, RRL_RATE_LIMIT, 0);
+	rrl = rrl_create(RRL_TABLE_SIZE, RRL_INSTANT_LIMIT, RRL_RATE_LIMIT, rw_mode, 0);
 	ok(rrl != NULL, "rrl(%s): create", impl_name);
 	assert(rrl);
 
@@ -276,7 +288,7 @@ void test_rrl(void)
 	count_test("IPv6 instant limit /64 not applied on /63", -1, 0,
 			AF_INET6, "8000:0:0:1::", 0, 0);
 
-	count_test("IPv6 instant limit /56", INST(V6, 56) - INST(V6, 64) - 1, 0,
+	count_test("IPv6 instant limit /56", INST(V6, 56) - INST(V6, 64) - 1, rw_mode ? 0 : 0.01,
 			AF_INET6, "8000:0:0:00%02x:%02x00::", 0x02, 0xff);
 
 	count_test("IPv6 instant limit /56 not applied on /55", -1, 0,
@@ -288,7 +300,7 @@ void test_rrl(void)
 	count_test("IPv6 instant limit /48 not applied on /47", -1, 0,
 			AF_INET6, "8000:0:1::", 0, 0);
 
-	count_test("IPv6 instant limit /32", INST(V6, 32) - INST(V6, 48) - 1, 0.001,
+	count_test("IPv6 instant limit /32", INST(V6, 32) - INST(V6, 48) - 1, rw_mode ? 0.001 : 0,
 			AF_INET6, "8000:0:%02x%02x::", 0x02, 0xff);
 
 	count_test("IPv6 instant limit /32 not applied on /31", -1, 0,
@@ -370,24 +382,36 @@ void test_rrl(void)
 	rrl_destroy(rrl);
 }
 
-int main(int argc, char *argv[])
+void test_rrl_mode(bool test_avx2, bool rw_mode)
 {
-	plan_lazy();
-
-	dnssec_crypto_init();
-
-	assert(KRU_GENERIC.initialize != KRU_AVX2.initialize);
-	bool test_avx2 = (KRU.initialize == KRU_AVX2.initialize);
+	if (!rw_mode) {
+		DIFF = 1;
+	}
 
 	KRU = KRU_GENERIC;
 	impl_name = "KRU_GENERIC";
-	test_rrl();
+	test_rrl(rw_mode);
 
 	if (test_avx2) {
 		KRU = KRU_AVX2;
 		impl_name = "KRU_AVX2";
-		test_rrl();
+		test_rrl(rw_mode);
+	} else {
+		diag("AVX2 NOT available");
 	}
+}
+
+int main(int argc, char *argv[])
+{
+	plan_lazy();
+
+	dnssec_crypto_init();
+
+	assert(KRU_GENERIC.initialize != KRU_AVX2.initialize);
+	bool test_avx2 = (KRU.initialize == KRU_AVX2.initialize);
+
+	test_rrl_mode(test_avx2, true);
+	test_rrl_mode(test_avx2, false);
 
 	dnssec_crypto_cleanup();
 	return 0;