diff --git a/Knot.files b/Knot.files
index 6ad59293bd7f08bc519201636ebfb1207921686f..2fc1bf67fa14cad2765ebafaf1692979ebeb0c40 100644
--- a/Knot.files
+++ b/Knot.files
@@ -218,6 +218,8 @@ src/knot/conf/confdb.c
 src/knot/conf/confdb.h
 src/knot/conf/confio.c
 src/knot/conf/confio.h
+src/knot/conf/migration.c
+src/knot/conf/migration.h
 src/knot/conf/scheme.c
 src/knot/conf/scheme.h
 src/knot/conf/tools.c
@@ -269,6 +271,10 @@ src/knot/modules/online_sign/online_sign.h
 src/knot/modules/rosedb/rosedb.c
 src/knot/modules/rosedb/rosedb.h
 src/knot/modules/rosedb/rosedb_tool.c
+src/knot/modules/rrl/functions.c
+src/knot/modules/rrl/functions.h
+src/knot/modules/rrl/rrl.c
+src/knot/modules/rrl/rrl.h
 src/knot/modules/stats/stats.c
 src/knot/modules/stats/stats.h
 src/knot/modules/synth_record/synth_record.c
@@ -308,8 +314,6 @@ src/knot/server/dthreads.c
 src/knot/server/dthreads.h
 src/knot/server/journal.c
 src/knot/server/journal.h
-src/knot/server/rrl.c
-src/knot/server/rrl.h
 src/knot/server/serialization.c
 src/knot/server/serialization.h
 src/knot/server/server.c
@@ -579,12 +583,12 @@ tests/libknot/test_yparser.c
 tests/libknot/test_ypscheme.c
 tests/libknot/test_yptrafo.c
 tests/modules/online_sign.c
+tests/modules/rrl.c
 tests/node.c
 tests/process_answer.c
 tests/process_query.c
 tests/query_module.c
 tests/requestor.c
-tests/rrl.c
 tests/server.c
 tests/test_conf.h
 tests/utils/test_cert.c
diff --git a/doc/configuration.rst b/doc/configuration.rst
index 3972d3893fad9382965e31fe0b57caf11f30048e..448523570402bafa8bd18b822cd03b57007189ff 100644
--- a/doc/configuration.rst
+++ b/doc/configuration.rst
@@ -247,31 +247,6 @@ processed::
         file: example.com.zone
         acl: update_acl
 
-Response rate limiting
-======================
-
-Response rate limiting (RRL) is a method to combat DNS reflection amplification
-attacks. These attacks rely on the fact that source address of a UDP query
-can be forged, and without a worldwide deployment of `BCP38
-<https://tools.ietf.org/html/bcp38>`_, such a forgery cannot be prevented.
-An attacker can use a DNS server (or multiple servers) as an amplification
-source and can flood a victim with a large number of unsolicited DNS responses.
-
-The RRL lowers the amplification factor of these attacks by sending some of
-the responses as truncated or by dropping them altogether.
-
-You can enable RRL by setting the :ref:`server_rate-limit` option in the
-:ref:`server section<Server section>`. The option controls how many responses
-per second are permitted for each flow. Responses exceeding this rate are
-limited. The option :ref:`server_rate-limit-slip` then configures how many
-limited responses are sent as truncated (slip) instead of being dropped.
-
-::
-
-    server:
-        rate-limit: 200     # Allow 200 resp/s for each flow
-        rate-limit-slip: 2  # Every other response slips
-
 .. _dnssec:
 
 Automatic DNSSEC signing
diff --git a/doc/man/knot.conf.5in b/doc/man/knot.conf.5in
index f03d0c5d71f8d7cba0debfe782f8d9e7fbf5c957..4bf887e091f2efb2e518e2e46b23ff738d34ef39 100644
--- a/doc/man/knot.conf.5in
+++ b/doc/man/knot.conf.5in
@@ -149,10 +149,6 @@ server:
     max\-udp\-payload: SIZE
     max\-ipv4\-udp\-payload: SIZE
     max\-ipv6\-udp\-payload: SIZE
-    rate\-limit: INT
-    rate\-limit\-slip: INT
-    rate\-limit\-table\-size: INT
-    rate\-limit\-whitelist: ADDR[/INT] | ADDR\-ADDR ...
     listen: ADDR[@INT] ...
 .ft P
 .fi
@@ -243,69 +239,6 @@ A maximum number of TCP clients connected in parallel, set this below the file
 descriptor limit to avoid resource exhaustion.
 .sp
 \fIDefault:\fP 100
-.SS rate\-limit
-.sp
-Rate limiting is based on the token bucket scheme. A rate basically
-represents a number of tokens available each second. Each response is
-processed and classified (based on several discriminators, e.g.
-source netblock, query type, zone name, rcode, etc.). Classified responses are
-then hashed and assigned to a bucket containing number of available
-tokens, timestamp and metadata. When available tokens are exhausted,
-response is dropped or sent as truncated (see \fI\%rate\-limit\-slip\fP).
-Number of available tokens is recalculated each second.
-.sp
-\fIDefault:\fP 0 (disabled)
-.SS rate\-limit\-table\-size
-.sp
-Size of the hash table in a number of buckets. The larger the hash table, the lesser
-the probability of a hash collision, but at the expense of additional memory costs.
-Each bucket is estimated roughly to 32 bytes. The size should be selected as
-a reasonably large prime due to better hash function distribution properties.
-Hash table is internally chained and works well up to a fill rate of 90 %, general
-rule of thumb is to select a prime near 1.2 * maximum_qps.
-.sp
-\fIDefault:\fP 393241
-.SS rate\-limit\-slip
-.sp
-As attacks using DNS/UDP are usually based on a forged source address,
-an attacker could deny services to the victim\(aqs netblock if all
-responses would be completely blocked. The idea behind SLIP mechanism
-is to send each N\s-2\uth\d\s0 response as truncated, thus allowing client to
-reconnect via TCP for at least some degree of service. It is worth
-noting, that some responses can\(aqt be truncated (e.g. SERVFAIL).
-.INDENT 0.0
-.IP \(bu 2
-Setting the value to \fB0\fP will cause that all rate\-limited responses will
-be dropped. The outbound bandwidth and packet rate will be strictly capped
-by the \fI\%rate\-limit\fP option. All legitimate requestors affected
-by the limit will face denial of service and will observe excessive timeouts.
-Therefore this setting is not recommended.
-.IP \(bu 2
-Setting the value to \fB1\fP will cause that all rate\-limited responses will
-be sent as truncated. The amplification factor of the attack will be reduced,
-but the outbound data bandwidth won\(aqt be lower than the incoming bandwidth.
-Also the outbound packet rate will be the same as without RRL.
-.IP \(bu 2
-Setting the value to \fB2\fP will cause that half of the rate\-limited responses
-will be dropped, the other half will be sent as truncated. With this
-configuration, both outbound bandwidth and packet rate will be lower than the
-inbound. On the other hand, the dropped responses enlarge the time window
-for possible cache poisoning attack on the resolver.
-.IP \(bu 2
-Setting the value to anything \fBlarger than 2\fP will keep on decreasing
-the outgoing rate\-limited bandwidth, packet rate, and chances to notify
-legitimate requestors to reconnect using TCP. These attributes are inversely
-proportional to the configured value. Setting the value high is not advisable.
-.UNINDENT
-.sp
-\fIDefault:\fP 1
-.SS rate\-limit\-whitelist
-.sp
-A list of IP addresses, network subnets, or network ranges to exempt from
-rate limiting. Empty list means that no incoming connection will be
-white\-listed.
-.sp
-\fIDefault:\fP not set
 .SS max\-udp\-payload
 .sp
 Maximum EDNS0 UDP payload size default for both IPv4 and IPv6.
@@ -1089,6 +1022,90 @@ Minimum severity level for messages related to zones that are logged.
 Minimum severity level for all message types that are logged.
 .sp
 \fIDefault:\fP not set
+.SH MODULE RRL
+.sp
+A response rate limiting module.
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+mod\-rrl:
+  \- id: STR
+    rate\-limit: INT
+    slip: INT
+    table\-size: INT
+    whitelist: ADDR[/INT] | ADDR\-ADDR ...
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.SS id
+.sp
+A module identifier.
+.SS rate\-limit
+.sp
+Rate limiting is based on the token bucket scheme. A rate basically
+represents a number of tokens available each second. Each response is
+processed and classified (based on several discriminators, e.g.
+source netblock, query type, zone name, rcode, etc.). Classified responses are
+then hashed and assigned to a bucket containing number of available
+tokens, timestamp and metadata. When available tokens are exhausted,
+response is dropped or sent as truncated (see \fI\%slip\fP).
+Number of available tokens is recalculated each second.
+.sp
+\fIRequired\fP
+.SS table\-size
+.sp
+Size of the hash table in a number of buckets. The larger the hash table, the lesser
+the probability of a hash collision, but at the expense of additional memory costs.
+Each bucket is estimated roughly to 32 bytes. The size should be selected as
+a reasonably large prime due to better hash function distribution properties.
+Hash table is internally chained and works well up to a fill rate of 90 %, general
+rule of thumb is to select a prime near 1.2 * maximum_qps.
+.sp
+\fIDefault:\fP 393241
+.SS slip
+.sp
+As attacks using DNS/UDP are usually based on a forged source address,
+an attacker could deny services to the victim\(aqs netblock if all
+responses would be completely blocked. The idea behind SLIP mechanism
+is to send each N\s-2\uth\d\s0 response as truncated, thus allowing client to
+reconnect via TCP for at least some degree of service. It is worth
+noting, that some responses can\(aqt be truncated (e.g. SERVFAIL).
+.INDENT 0.0
+.IP \(bu 2
+Setting the value to \fB0\fP will cause that all rate\-limited responses will
+be dropped. The outbound bandwidth and packet rate will be strictly capped
+by the \fI\%rate\-limit\fP option. All legitimate requestors affected
+by the limit will face denial of service and will observe excessive timeouts.
+Therefore this setting is not recommended.
+.IP \(bu 2
+Setting the value to \fB1\fP will cause that all rate\-limited responses will
+be sent as truncated. The amplification factor of the attack will be reduced,
+but the outbound data bandwidth won\(aqt be lower than the incoming bandwidth.
+Also the outbound packet rate will be the same as without RRL.
+.IP \(bu 2
+Setting the value to \fB2\fP will cause that half of the rate\-limited responses
+will be dropped, the other half will be sent as truncated. With this
+configuration, both outbound bandwidth and packet rate will be lower than the
+inbound. On the other hand, the dropped responses enlarge the time window
+for possible cache poisoning attack on the resolver.
+.IP \(bu 2
+Setting the value to anything \fBlarger than 2\fP will keep on decreasing
+the outgoing rate\-limited bandwidth, packet rate, and chances to notify
+legitimate requestors to reconnect using TCP. These attributes are inversely
+proportional to the configured value. Setting the value high is not advisable.
+.UNINDENT
+.sp
+\fIDefault:\fP 1
+.SS whitelist
+.sp
+A list of IP addresses, network subnets, or network ranges to exempt from
+rate limiting. Empty list means that no incoming connection will be
+white\-listed.
+.sp
+\fIDefault:\fP not set
 .SH MODULE DNSTAP
 .sp
 The module dnstap allows query and response logging.
diff --git a/doc/modules.rst b/doc/modules.rst
index 1fca0c8481cb2d6b1cca6677435d77d3ec7bf7a3..5cc3cf6bb71e38dc782f45df0467588a760c3c1c 100644
--- a/doc/modules.rst
+++ b/doc/modules.rst
@@ -31,7 +31,36 @@ an identifier must be created and then referenced in the form of
 
 .. NOTE::
    Query modules are processed in the order they are specified in the
-   zone/template configuration.
+   zone/template configuration. In most cases, the recommended order is::
+
+      mod-synth-record, mod-online-sign, mod-rrl, mod-dnstap, mod-stats
+
+``rrl`` — Response rate limiting
+--------------------------------
+
+Response rate limiting (RRL) is a method to combat DNS reflection amplification
+attacks. These attacks rely on the fact that source address of a UDP query
+can be forged, and without a worldwide deployment of `BCP38
+<https://tools.ietf.org/html/bcp38>`_, such a forgery cannot be prevented.
+An attacker can use a DNS server (or multiple servers) as an amplification
+source and can flood a victim with a large number of unsolicited DNS responses.
+The RRL lowers the amplification factor of these attacks by sending some of
+the responses as truncated or by dropping them altogether.
+
+The module introduces two counters. The number of slipped and dropped responses.
+
+You can enable RRL by setting the :ref:`mod-rrl<mod-rrl>` module globally or per zone.
+
+::
+
+    mod-rrl:
+      - id: default
+        rate-limit: 200   # Allow 200 resp/s for each flow
+        slip: 2           # Every other response slips
+
+    template:
+      - id: default
+        global-module: mod-rrl/default   # Enable RRL globally
 
 ``dnstap`` – dnstap-enabled query logging
 -----------------------------------------
@@ -503,7 +532,7 @@ AAAA-only glue records.
 ------------------------
 
 The module sends empty truncated response to any UDP query. This is similar
-to a slipped answer in :ref:`response rate limiting<server_rate-limit>`.
+to a slipped answer in :ref:`response rate limiting<mod-rrl_rate-limit>`.
 TCP queries are not affected.
 
 To enable this module globally, you need to add something like the following
diff --git a/doc/reference.rst b/doc/reference.rst
index 783c421b6c3385dfd7d8c1b279c39a142c34f622..2087261383152863c9eadb663db966d8afd0e407 100644
--- a/doc/reference.rst
+++ b/doc/reference.rst
@@ -102,10 +102,6 @@ General options related to the server.
      max-udp-payload: SIZE
      max-ipv4-udp-payload: SIZE
      max-ipv6-udp-payload: SIZE
-     rate-limit: INT
-     rate-limit-slip: INT
-     rate-limit-table-size: INT
-     rate-limit-whitelist: ADDR[/INT] | ADDR-ADDR ...
      listen: ADDR[@INT] ...
 
 .. _server_identity:
@@ -249,83 +245,6 @@ descriptor limit to avoid resource exhaustion.
 
 *Default:* 100
 
-.. _server_rate-limit:
-
-rate-limit
-----------
-
-Rate limiting is based on the token bucket scheme. A rate basically
-represents a number of tokens available each second. Each response is
-processed and classified (based on several discriminators, e.g.
-source netblock, query type, zone name, rcode, etc.). Classified responses are
-then hashed and assigned to a bucket containing number of available
-tokens, timestamp and metadata. When available tokens are exhausted,
-response is dropped or sent as truncated (see :ref:`server_rate-limit-slip`).
-Number of available tokens is recalculated each second.
-
-*Default:* 0 (disabled)
-
-.. _server_rate-limit-table-size:
-
-rate-limit-table-size
----------------------
-
-Size of the hash table in a number of buckets. The larger the hash table, the lesser
-the probability of a hash collision, but at the expense of additional memory costs.
-Each bucket is estimated roughly to 32 bytes. The size should be selected as
-a reasonably large prime due to better hash function distribution properties.
-Hash table is internally chained and works well up to a fill rate of 90 %, general
-rule of thumb is to select a prime near 1.2 * maximum_qps.
-
-*Default:* 393241
-
-.. _server_rate-limit-slip:
-
-rate-limit-slip
----------------
-
-As attacks using DNS/UDP are usually based on a forged source address,
-an attacker could deny services to the victim's netblock if all
-responses would be completely blocked. The idea behind SLIP mechanism
-is to send each N\ :sup:`th` response as truncated, thus allowing client to
-reconnect via TCP for at least some degree of service. It is worth
-noting, that some responses can't be truncated (e.g. SERVFAIL).
-
-- Setting the value to **0** will cause that all rate-limited responses will
-  be dropped. The outbound bandwidth and packet rate will be strictly capped
-  by the :ref:`server_rate-limit` option. All legitimate requestors affected
-  by the limit will face denial of service and will observe excessive timeouts.
-  Therefore this setting is not recommended.
-
-- Setting the value to **1** will cause that all rate-limited responses will
-  be sent as truncated. The amplification factor of the attack will be reduced,
-  but the outbound data bandwidth won't be lower than the incoming bandwidth.
-  Also the outbound packet rate will be the same as without RRL.
-
-- Setting the value to **2** will cause that half of the rate-limited responses
-  will be dropped, the other half will be sent as truncated. With this
-  configuration, both outbound bandwidth and packet rate will be lower than the
-  inbound. On the other hand, the dropped responses enlarge the time window
-  for possible cache poisoning attack on the resolver.
-
-- Setting the value to anything **larger than 2** will keep on decreasing
-  the outgoing rate-limited bandwidth, packet rate, and chances to notify
-  legitimate requestors to reconnect using TCP. These attributes are inversely
-  proportional to the configured value. Setting the value high is not advisable.
-
-*Default:* 1
-
-.. _server_rate-limit-whitelist:
-
-rate-limit-whitelist
---------------------
-
-A list of IP addresses, network subnets, or network ranges to exempt from
-rate limiting. Empty list means that no incoming connection will be
-white-listed.
-
-*Default:* not set
-
 .. _server_max-udp-payload:
 
 max-udp-payload
@@ -1274,6 +1193,106 @@ Minimum severity level for all message types that are logged.
 
 *Default:* not set
 
+.. _mod-rrl:
+
+Module rrl
+==========
+
+A response rate limiting module.
+
+::
+
+ mod-rrl:
+   - id: STR
+     rate-limit: INT
+     slip: INT
+     table-size: INT
+     whitelist: ADDR[/INT] | ADDR-ADDR ...
+
+.. _mod-rrl_id:
+
+id
+--
+
+A module identifier.
+
+.. _mod-rrl_rate-limit:
+
+rate-limit
+----------
+
+Rate limiting is based on the token bucket scheme. A rate basically
+represents a number of tokens available each second. Each response is
+processed and classified (based on several discriminators, e.g.
+source netblock, query type, zone name, rcode, etc.). Classified responses are
+then hashed and assigned to a bucket containing number of available
+tokens, timestamp and metadata. When available tokens are exhausted,
+response is dropped or sent as truncated (see :ref:`mod-rrl_slip`).
+Number of available tokens is recalculated each second.
+
+*Required*
+
+.. _mod-rrl_table-size:
+
+table-size
+----------
+
+Size of the hash table in a number of buckets. The larger the hash table, the lesser
+the probability of a hash collision, but at the expense of additional memory costs.
+Each bucket is estimated roughly to 32 bytes. The size should be selected as
+a reasonably large prime due to better hash function distribution properties.
+Hash table is internally chained and works well up to a fill rate of 90 %, general
+rule of thumb is to select a prime near 1.2 * maximum_qps.
+
+*Default:* 393241
+
+.. _mod-rrl_slip:
+
+slip
+----
+
+As attacks using DNS/UDP are usually based on a forged source address,
+an attacker could deny services to the victim's netblock if all
+responses would be completely blocked. The idea behind SLIP mechanism
+is to send each N\ :sup:`th` response as truncated, thus allowing client to
+reconnect via TCP for at least some degree of service. It is worth
+noting, that some responses can't be truncated (e.g. SERVFAIL).
+
+- Setting the value to **0** will cause that all rate-limited responses will
+  be dropped. The outbound bandwidth and packet rate will be strictly capped
+  by the :ref:`mod-rrl_rate-limit` option. All legitimate requestors affected
+  by the limit will face denial of service and will observe excessive timeouts.
+  Therefore this setting is not recommended.
+
+- Setting the value to **1** will cause that all rate-limited responses will
+  be sent as truncated. The amplification factor of the attack will be reduced,
+  but the outbound data bandwidth won't be lower than the incoming bandwidth.
+  Also the outbound packet rate will be the same as without RRL.
+
+- Setting the value to **2** will cause that half of the rate-limited responses
+  will be dropped, the other half will be sent as truncated. With this
+  configuration, both outbound bandwidth and packet rate will be lower than the
+  inbound. On the other hand, the dropped responses enlarge the time window
+  for possible cache poisoning attack on the resolver.
+
+- Setting the value to anything **larger than 2** will keep on decreasing
+  the outgoing rate-limited bandwidth, packet rate, and chances to notify
+  legitimate requestors to reconnect using TCP. These attributes are inversely
+  proportional to the configured value. Setting the value high is not advisable.
+
+*Default:* 1
+
+.. _mod-rrl_whitelist:
+
+whitelist
+---------
+
+A list of IP addresses, network subnets, or network ranges to exempt from
+rate limiting. Empty list means that no incoming connection will be
+white-listed.
+
+*Default:* not set
+
 .. _Module dnstap:
 
 Module dnstap
diff --git a/src/Makefile.am b/src/Makefile.am
index 406b0089a490c35dbc49cb64b9db5140ecac85bd..183e0d29d3639d562e80365e555913d2b193b11f 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -234,6 +234,8 @@ libknotd_la_SOURCES =				\
 	knot/conf/confdb.h			\
 	knot/conf/confio.c			\
 	knot/conf/confio.h			\
+	knot/conf/migration.c			\
+	knot/conf/migration.h			\
 	knot/conf/scheme.c			\
 	knot/conf/scheme.h			\
 	knot/conf/tools.c			\
@@ -280,6 +282,10 @@ libknotd_la_SOURCES =				\
 	knot/modules/online_sign/online_sign.h	\
 	knot/modules/online_sign/nsec_next.c	\
 	knot/modules/online_sign/nsec_next.h	\
+	knot/modules/rrl/functions.c		\
+	knot/modules/rrl/functions.h		\
+	knot/modules/rrl/rrl.c			\
+	knot/modules/rrl/rrl.h			\
 	knot/modules/stats/stats.c		\
 	knot/modules/stats/stats.h		\
 	knot/modules/synth_record/synth_record.c\
@@ -331,8 +337,6 @@ libknotd_la_SOURCES =				\
 	knot/server/dthreads.h			\
 	knot/server/journal.c			\
 	knot/server/journal.h			\
-	knot/server/rrl.c			\
-	knot/server/rrl.h			\
 	knot/server/serialization.c		\
 	knot/server/serialization.h		\
 	knot/server/server.c			\
diff --git a/src/knot/conf/base.c b/src/knot/conf/base.c
index 8918022dc95fcad34ae61679158771adcb70bf5b..15e6f1523a66e07df0300656791b17a0baad1621 100644
--- a/src/knot/conf/base.c
+++ b/src/knot/conf/base.c
@@ -133,15 +133,10 @@ static void init_cache(
 	val = conf_get(conf, C_SRV, C_MAX_TCP_CLIENTS);
 	conf->cache.srv_max_tcp_clients = conf_int(&val);
 
-	val = conf_get(conf, C_SRV, C_RATE_LIMIT_SLIP);
-	conf->cache.srv_rate_limit_slip = conf_int(&val);
-
 	val = conf_get(conf, C_CTL, C_TIMEOUT);
 	conf->cache.ctl_timeout = conf_int(&val) * 1000;
 
 	conf->cache.srv_nsid = conf_get(conf, C_SRV, C_NSID);
-
-	conf->cache.srv_rate_limit_whitelist = conf_get(conf, C_SRV, C_RATE_LIMIT_WHITELIST);
 }
 
 int conf_new(
diff --git a/src/knot/conf/base.h b/src/knot/conf/base.h
index 4b803c2a9fdc7ee2726ce7f2851343eecb8a89fd..8105b2076935e1a3e4d9c462592ab9db1a3b97fb 100644
--- a/src/knot/conf/base.h
+++ b/src/knot/conf/base.h
@@ -106,10 +106,8 @@ typedef struct {
 		int32_t srv_tcp_idle_timeout;
 		int32_t srv_tcp_reply_timeout;
 		int32_t srv_max_tcp_clients;
-		int32_t srv_rate_limit_slip;
 		int32_t ctl_timeout;
 		conf_val_t srv_nsid;
-		conf_val_t srv_rate_limit_whitelist;
 	} cache;
 
 	/*! List of active query modules. */
diff --git a/src/knot/conf/migration.c b/src/knot/conf/migration.c
new file mode 100644
index 0000000000000000000000000000000000000000..08099ce3e4bf2b9f66977ef4b54c3e4818d0424f
--- /dev/null
+++ b/src/knot/conf/migration.c
@@ -0,0 +1,133 @@
+/*  Copyright (C) 2016 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "knot/common/log.h"
+#include "knot/conf/migration.h"
+#include "knot/conf/confdb.h"
+
+static void try_unset(conf_t *conf, knot_db_txn_t *txn, yp_name_t *key0, yp_name_t *key1)
+{
+	int ret = conf_db_unset(conf, txn, key0, key1, NULL, 0, NULL, 0, true);
+	if (ret != KNOT_EOK && ret != KNOT_ENOENT) {
+		log_warning("conf, migration, failed to unset '%s%s%s' (%s)",
+		            key0 + 1,
+		            (key1 != NULL) ? "/"      : "",
+		            (key1 != NULL) ? key1 + 1 : "",
+		            knot_strerror(ret));
+	}
+}
+
+#define check_set(conf, txn, key0, key1, id, id_len, data, data_len) \
+	ret = conf_db_set(conf, txn, key0, key1, id, id_len, data, data_len); \
+	if (ret != KNOT_EOK && ret != KNOT_CONF_EREDEFINE) { \
+		log_error("conf, migration, failed to set '%s%s%s' (%s)", \
+		          key0 + 1, \
+		          (key1 != NULL) ? "/"      : "", \
+		          (key1 != NULL) ? key1 + 1 : "", \
+		          knot_strerror(ret)); \
+		return ret; \
+	}
+
+static int migrate_rrl(
+	conf_t *conf,
+	knot_db_txn_t *txn)
+{
+	#define MOD_RRL		"\x07""mod-rrl"
+	#define MOD_RATE_LIMIT	"\x0A""rate-limit"
+	#define MOD_SLIP	"\x04""slip"
+	#define MOD_TBL_SIZE	"\x0A""table-size"
+	#define MOD_WHITELIST	"\x09""whitelist"
+
+	const uint8_t *id = CONF_DEFAULT_ID + 1;
+	const size_t id_len = CONF_DEFAULT_ID[0];
+	const uint8_t *dflt_rrl = (const uint8_t *)MOD_RRL "default\0";
+	const size_t dflt_rrl_len = 16;
+
+	conf_val_t val;
+	int ret = conf_db_get(conf, txn, C_SRV, C_RATE_LIMIT, NULL, 0, &val);
+
+	// Migrate old configuration if RRL enabled.
+	if (ret == KNOT_EOK && conf_int(&val) > 0) {
+		log_notice("config, migrating RRL configuration from server to mod-rrl");
+
+		// Create equivalent mod-rrl configuration.
+		check_set(conf, txn, MOD_RRL, C_ID, id, id_len, NULL, 0);
+		check_set(conf, txn, MOD_RRL, MOD_RATE_LIMIT, id, id_len,
+		          val.data, val.len);
+
+		conf_db_get(conf, txn, C_SRV, C_RATE_LIMIT_SLIP, NULL, 0, &val);
+		if (val.code == KNOT_EOK) {
+			conf_val(&val);
+			check_set(conf, txn, MOD_RRL, MOD_SLIP, id, id_len,
+			          val.data, val.len);
+		}
+
+		conf_db_get(conf, txn, C_SRV, C_RATE_LIMIT_TBL_SIZE, NULL, 0, &val);
+		if (val.code == KNOT_EOK) {
+			conf_val(&val);
+			check_set(conf, txn, MOD_RRL, MOD_TBL_SIZE, id, id_len,
+			          val.data, val.len);
+		}
+
+		conf_db_get(conf, txn, C_SRV, C_RATE_LIMIT_WHITELIST, NULL, 0, &val);
+		while (val.code == KNOT_EOK) {
+			conf_val(&val);
+			check_set(conf, txn, MOD_RRL, MOD_WHITELIST, id, id_len,
+			          val.data, val.len);
+			conf_val_next(&val);
+		}
+
+		// Create default template and assing global module.
+		check_set(conf, txn, C_TPL, C_ID, id, id_len, NULL, 0);
+		check_set(conf, txn, C_TPL, C_GLOBAL_MODULE, id, id_len,
+		          dflt_rrl, dflt_rrl_len);
+	}
+
+	// Drop old RRL configuration.
+	try_unset(conf, txn, C_SRV, C_RATE_LIMIT);
+	try_unset(conf, txn, C_SRV, C_RATE_LIMIT_SLIP);
+	try_unset(conf, txn, C_SRV, C_RATE_LIMIT_TBL_SIZE);
+	try_unset(conf, txn, C_SRV, C_RATE_LIMIT_WHITELIST);
+
+	return KNOT_EOK;
+}
+
+int conf_migrate(
+	conf_t *conf)
+{
+	if (conf == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	knot_db_txn_t txn;
+	int ret = conf->api->txn_begin(conf->db, &txn, 0);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	ret = migrate_rrl(conf, &txn);
+	if (ret != KNOT_EOK) {
+		conf->api->txn_abort(&txn);
+		return ret;
+	}
+
+	ret = conf->api->txn_commit(&txn);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	return conf_refresh_txn(conf);
+}
diff --git a/src/knot/conf/migration.h b/src/knot/conf/migration.h
new file mode 100644
index 0000000000000000000000000000000000000000..dd7912f70d09cd5bd1e2fa6a50a2f37efc467159
--- /dev/null
+++ b/src/knot/conf/migration.h
@@ -0,0 +1,30 @@
+/*  Copyright (C) 2016 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#pragma once
+
+#include "knot/conf/base.h"
+
+/*!
+ * Migrates from an old configuration schema.
+ *
+ * \param[in] conf  Configuration.
+ *
+ * \return Error code, KNOT_EOK if success.
+ */
+int conf_migrate(
+	conf_t *conf
+);
diff --git a/src/knot/conf/scheme.c b/src/knot/conf/scheme.c
index af4e0596295563f94e67ec06701a50712ae7b440..566feaffe67675f6f220360104c6967f41414000 100644
--- a/src/knot/conf/scheme.c
+++ b/src/knot/conf/scheme.c
@@ -23,12 +23,12 @@
 #include "knot/conf/confio.h"
 #include "knot/conf/tools.h"
 #include "knot/common/log.h"
-#include "knot/server/rrl.h"
 #include "knot/updates/acl.h"
 #include "libknot/rrtype/opt.h"
 #include "dnssec/lib/dnssec/tsig.h"
 #include "dnssec/lib/dnssec/key.h"
 
+#include "knot/modules/rrl/rrl.h"
 #include "knot/modules/stats/stats.h"
 #include "knot/modules/synth_record/synth_record.h"
 #include "knot/modules/dnsproxy/dnsproxy.h"
@@ -122,13 +122,14 @@ static const yp_item_t desc_server[] = {
 	{ C_MAX_IPV6_UDP_PAYLOAD, YP_TINT,  YP_VINT = { KNOT_EDNS_MIN_UDP_PAYLOAD,
 	                                                KNOT_EDNS_MAX_UDP_PAYLOAD,
 	                                                4096, YP_SSIZE } },
+	{ C_LISTEN,               YP_TADDR, YP_VADDR = { 53 }, YP_FMULTI },
+	{ C_COMMENT,              YP_TSTR,  YP_VNONE },
+	/* Obsolete items. */
 	{ C_RATE_LIMIT,           YP_TINT,  YP_VINT = { 0, INT32_MAX, 0 } },
-	{ C_RATE_LIMIT_SLIP,      YP_TINT,  YP_VINT = { 0, RRL_SLIP_MAX, 1 } },
+	{ C_RATE_LIMIT_SLIP,      YP_TINT,  YP_VINT = { 0, 100, 1 } },
 	{ C_RATE_LIMIT_TBL_SIZE,  YP_TINT,  YP_VINT = { 1, INT32_MAX, 393241 } },
 	{ C_RATE_LIMIT_WHITELIST, YP_TDATA, YP_VDATA = { 0, NULL, addr_range_to_bin,
 	                                                 addr_range_to_txt }, YP_FMULTI },
-	{ C_LISTEN,               YP_TADDR, YP_VADDR = { 53 }, YP_FMULTI },
-	{ C_COMMENT,              YP_TSTR,  YP_VNONE },
 	{ NULL }
 };
 
@@ -266,7 +267,7 @@ static const yp_item_t desc_zone[] = {
 };
 
 const yp_item_t conf_scheme[] = {
-	{ C_SRV,      YP_TGRP, YP_VGRP = { desc_server }, CONF_IO_FRLD_SRV },
+	{ C_SRV,      YP_TGRP, YP_VGRP = { desc_server }, CONF_IO_FRLD_SRV, { check_server } },
 	{ C_CTL,      YP_TGRP, YP_VGRP = { desc_control } },
 	{ C_LOG,      YP_TGRP, YP_VGRP = { desc_log }, YP_FMULTI | CONF_IO_FRLD_LOG },
 	{ C_STATS,    YP_TGRP, YP_VGRP = { desc_stats }, CONF_IO_FRLD_SRV },
@@ -276,6 +277,8 @@ const yp_item_t conf_scheme[] = {
 	{ C_ACL,      YP_TGRP, YP_VGRP = { desc_acl }, YP_FMULTI, { check_acl } },
 	{ C_RMT,      YP_TGRP, YP_VGRP = { desc_remote }, YP_FMULTI, { check_remote } },
 /* MODULES */
+	{ C_MOD_RRL,          YP_TGRP, YP_VGRP = { scheme_mod_rrl }, FMOD,
+	                                         { check_mod_rrl } },
 	{ C_MOD_STATS,        YP_TGRP, YP_VGRP = { scheme_mod_stats }, FMOD },
 	{ C_MOD_SYNTH_RECORD, YP_TGRP, YP_VGRP = { scheme_mod_synth_record }, FMOD,
 	                               { check_mod_synth_record } },
diff --git a/src/knot/conf/tools.c b/src/knot/conf/tools.c
index 2823711700a6cec6cbc3e6dd183907ee74d3a359..14127c198a20c967abbb091c17d98a8ade9cd092 100644
--- a/src/knot/conf/tools.c
+++ b/src/knot/conf/tools.c
@@ -353,6 +353,40 @@ int check_modref(
 	return KNOT_EOK;
 }
 
+int check_server(
+	conf_check_t *args)
+{
+	bool present = false;
+
+	conf_val_t val;
+	val = conf_get_txn(args->conf, args->txn, C_SRV, C_RATE_LIMIT);
+	if (val.code == KNOT_EOK) {
+		present = true;
+	}
+
+	val = conf_get_txn(args->conf, args->txn, C_SRV, C_RATE_LIMIT_SLIP);
+	if (val.code == KNOT_EOK) {
+		present = true;
+	}
+
+	val = conf_get_txn(args->conf, args->txn, C_SRV, C_RATE_LIMIT_TBL_SIZE);
+	if (val.code == KNOT_EOK) {
+		present = true;
+	}
+
+	val = conf_get_txn(args->conf, args->txn, C_SRV, C_RATE_LIMIT_WHITELIST);
+	if (val.code == KNOT_EOK) {
+		present = true;
+	}
+
+	if (present) {
+		CONF_LOG(LOG_NOTICE, "obsolete RRL configuration in the server, "
+		                     "use module mod-rrl instead");
+	}
+
+	return KNOT_EOK;
+}
+
 int check_keystore(
 	conf_check_t *args)
 {
@@ -503,8 +537,8 @@ int check_zone(
 	                                       C_DNSSEC_POLICY, args->id);
 	if (conf_bool(&signing) && policy.code != KNOT_EOK) {
 		CONF_LOG(LOG_NOTICE, "DNSSEC policy settings in KASP database "
-		         "is obsolete and will be removed in the next major release. "
-		         "Use zone.dnssec-policy in server configuration instead.");
+		         "is obsolete and will be removed in the next major release, "
+		         "use zone.dnssec-policy in server configuration instead");
 	}
 
 	return KNOT_EOK;
diff --git a/src/knot/conf/tools.h b/src/knot/conf/tools.h
index 8378a0b42d0d5a6617fa2991d2b349011b1be036..041d360e5efa4d1841d28fde4e487913f82564bf 100644
--- a/src/knot/conf/tools.h
+++ b/src/knot/conf/tools.h
@@ -84,6 +84,10 @@ int check_modref(
 	conf_check_t *args
 );
 
+int check_server(
+	conf_check_t *args
+);
+
 int check_keystore(
 	conf_check_t *args
 );
diff --git a/src/knot/server/rrl.c b/src/knot/modules/rrl/functions.c
similarity index 91%
rename from src/knot/server/rrl.c
rename to src/knot/modules/rrl/functions.c
index 27026f8a09a7500ca57d06a2378abbcb2816be32..df6cdd56250e1413a516a71268b99165acbacd26 100644
--- a/src/knot/server/rrl.c
+++ b/src/knot/modules/rrl/functions.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2011 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2016 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -17,13 +17,11 @@
 #include <assert.h>
 #include <time.h>
 
-#include "dnssec/random.h"
-#include "knot/common/log.h"
-#include "knot/server/rrl.h"
-#include "knot/zone/zone.h"
-#include "libknot/libknot.h"
 #include "contrib/murmurhash3/murmurhash3.h"
 #include "contrib/sockaddr.h"
+#include "dnssec/random.h"
+#include "knot/modules/rrl/functions.h"
+#include "knot/common/log.h"
 
 /* Hopscotch defines. */
 #define HOP_LEN (sizeof(unsigned)*8)
@@ -33,7 +31,6 @@
 #define RRL_V4_PREFIX ((uint32_t)0x00ffffff)         /* /24 */
 #define RRL_V6_PREFIX ((uint64_t)0x00ffffffffffffff) /* /56 */
 /* Defaults */
-#define RRL_DEFAULT_RATE 100
 #define RRL_CAPACITY 4 /* N seconds. */
 #define RRL_SSTART 2 /* 1/Nth of the rate for slow start */
 #define RRL_PSIZE_LARGE 1024
@@ -60,16 +57,16 @@ struct cls_name {
 };
 
 static const struct cls_name rrl_cls_names[] = {
-        {CLS_NORMAL,  "POSITIVE" },
-        {CLS_ERROR,   "ERROR" },
-        {CLS_NXDOMAIN,"NXDOMAIN"},
-        {CLS_EMPTY,   "EMPTY"},
-        {CLS_LARGE,   "LARGE"},
-        {CLS_WILDCARD,"WILDCARD"},
-        {CLS_ANY,     "ANY"},
-        {CLS_DNSSEC,  "DNSSEC"},
-        {CLS_NULL,    "NULL"},
-        {CLS_NULL,    NULL}
+	{ CLS_NORMAL,   "POSITIVE" },
+	{ CLS_ERROR,    "ERROR" },
+	{ CLS_NXDOMAIN, "NXDOMAIN"},
+	{ CLS_EMPTY,    "EMPTY"},
+	{ CLS_LARGE,    "LARGE"},
+	{ CLS_WILDCARD, "WILDCARD"},
+	{ CLS_ANY,      "ANY"},
+	{ CLS_DNSSEC,   "DNSSEC"},
+	{ CLS_NULL,     "NULL"},
+	{ CLS_NULL,     NULL}
 };
 
 static inline const char *rrl_clsstr(int code)
@@ -134,15 +131,12 @@ static uint8_t rrl_clsid(rrl_req_t *p)
 	return ret;
 }
 
-static int rrl_clsname(char *dst, size_t maxlen, uint8_t cls,
-                       rrl_req_t *req, const zone_t *zone)
+static int rrl_clsname(char *dst, size_t maxlen, uint8_t cls, rrl_req_t *req,
+                       const knot_dname_t *name)
 {
-	/* Fallback zone (for errors etc.) */
-	const knot_dname_t *dn = (const knot_dname_t *)"\x00";
-
-	/* Found associated zone. */
-	if (zone != NULL) {
-		dn = zone->name;
+	if (name == NULL) {
+		/* Fallback for errors etc. */
+		name = (const knot_dname_t *)"\x00";
 	}
 
 	switch (cls) {
@@ -153,17 +147,17 @@ static int rrl_clsname(char *dst, size_t maxlen, uint8_t cls,
 	default:
 		/* Use QNAME */
 		if (req->query) {
-			dn = knot_pkt_qname(req->query);
+			name = knot_pkt_qname(req->query);
 		}
 		break;
 	}
 
 	/* Write to wire */
-	return knot_dname_to_wire((uint8_t *)dst, dn, maxlen);
+	return knot_dname_to_wire((uint8_t *)dst, name, maxlen);
 }
 
 static int rrl_classify(char *dst, size_t maxlen, const struct sockaddr_storage *a,
-                        rrl_req_t *p, const zone_t *z, uint32_t seed)
+                        rrl_req_t *p, const knot_dname_t *z, uint32_t seed)
 {
 	if (!dst || !p || !a || maxlen == 0) {
 		return KNOT_EINVAL;
@@ -295,7 +289,7 @@ static void rrl_log_state(const struct sockaddr_storage *ss, uint16_t flags, uin
 		what = "enters";
 	}
 
-	log_notice("rate limiting, address '%s' class '%s' %s limiting",
+	log_notice("mod-rrl, address '%s' class '%s' %s limiting",
 	           addr_str, rrl_clsstr(cls), what);
 #endif
 }
@@ -375,7 +369,7 @@ int rrl_setlocks(rrl_table_t *rrl, unsigned granularity)
 }
 
 rrl_item_t *rrl_hash(rrl_table_t *t, const struct sockaddr_storage *a, rrl_req_t *p,
-                     const zone_t *zone, uint32_t stamp, int *lock)
+                     const knot_dname_t *zone, uint32_t stamp, int *lock)
 {
 	char buf[RRL_CLSBLK_MAXLEN];
 	int len = rrl_classify(buf, sizeof(buf), a, p, zone, t->seed);
@@ -437,7 +431,7 @@ rrl_item_t *rrl_hash(rrl_table_t *t, const struct sockaddr_storage *a, rrl_req_t
 }
 
 int rrl_query(rrl_table_t *rrl, const struct sockaddr_storage *a, rrl_req_t *req,
-              const zone_t *zone)
+              const knot_dname_t *zone)
 {
 	if (!rrl || !req || !a) {
 		return KNOT_EINVAL;
diff --git a/src/knot/server/rrl.h b/src/knot/modules/rrl/functions.h
similarity index 81%
rename from src/knot/server/rrl.h
rename to src/knot/modules/rrl/functions.h
index 6d01ff0b0c70e77178a5c64a64776d404098bbca..a81fa02da59cae7a48d12aaba7e4f58a1b58a55b 100644
--- a/src/knot/server/rrl.h
+++ b/src/knot/modules/rrl/functions.h
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2011 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2016 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -13,22 +13,14 @@
     You should have received a copy of the GNU General Public License
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
-/*!
- * \file rrl.h
- *
- * \author Marek Vavrusa <marek.vavusa@nic.cz>
- *
- * \brief Response-rate limiting API.
- *
- * \addtogroup network
- * @{
- */
 
 #pragma once
 
 #include <stdint.h>
 #include <pthread.h>
 #include <sys/socket.h>
+
+#include "libknot/dname.h"
 #include "libknot/packet/pkt.h"
 
 /* Defaults */
@@ -41,19 +33,17 @@ enum {
 	RRL_WILDCARD  = 1 << 1  /*!< Query to wildcard name. */
 };
 
-struct zone;
-
 /*!
  * \brief RRL hash bucket.
  */
-typedef struct rrl_item {
+typedef struct {
 	unsigned hop;        /* Hop bitmap. */
 	uint64_t netblk;     /* Prefix associated. */
-	uint16_t ntok;       /* Tokens available */
-	uint8_t  cls;        /* Bucket class */
-	uint8_t  flags;      /* Flags */
-	uint32_t qname;      /* imputed(QNAME) hash */
-	uint32_t time;       /* Timestamp */
+	uint16_t ntok;       /* Tokens available. */
+	uint8_t  cls;        /* Bucket class. */
+	uint8_t  flags;      /* Flags. */
+	uint32_t qname;      /* imputed(QNAME) hash. */
+	uint32_t time;       /* Timestamp. */
 } rrl_item_t;
 
 /*!
@@ -68,20 +58,20 @@ typedef struct rrl_item {
  * As of now lock K for bucket N is calculated as K = N % (num_buckets).
  */
 
-typedef struct rrl_table {
-	uint32_t rate;       /* Configured RRL limit */
+typedef struct {
+	uint32_t rate;       /* Configured RRL limit. */
 	uint32_t seed;       /* Pseudorandom seed for hashing. */
 	pthread_mutex_t ll;
-	pthread_mutex_t *lk;      /* Table locks. */
+	pthread_mutex_t *lk; /* Table locks. */
 	unsigned lk_count;   /* Table lock count (granularity). */
-	size_t size;         /* Number of buckets */
-	rrl_item_t arr[];    /* Buckets */
+	size_t size;         /* Number of buckets. */
+	rrl_item_t arr[];    /* Buckets. */
 } rrl_table_t;
 
 /*!
  * \brief RRL request descriptor.
  */
-typedef struct rrl_req {
+typedef struct {
 	const uint8_t *w;
 	uint16_t len;
 	unsigned flags;
@@ -128,13 +118,13 @@ int rrl_setlocks(rrl_table_t *rrl, unsigned granularity);
  * \param t RRL table.
  * \param a Source address.
  * \param p RRL request.
- * \param zone Relate zone.
+ * \param zone Relate zone name.
  * \param stamp Timestamp (current time).
  * \param lock Held lock.
  * \return assigned bucket
  */
-rrl_item_t* rrl_hash(rrl_table_t *t, const struct sockaddr_storage *a, rrl_req_t *p,
-                     const struct zone *zone, uint32_t stamp, int* lock);
+rrl_item_t *rrl_hash(rrl_table_t *t, const struct sockaddr_storage *a, rrl_req_t *p,
+                     const knot_dname_t *zone, uint32_t stamp, int *lock);
 
 /*!
  * \brief Query the RRL table for accept or deny, when the rate limit is reached.
@@ -142,12 +132,12 @@ rrl_item_t* rrl_hash(rrl_table_t *t, const struct sockaddr_storage *a, rrl_req_t
  * \param rrl RRL table.
  * \param a Source address.
  * \param req RRL request (containing resp., flags and question).
- * \param zone Zone related to the response (or NULL).
+ * \param zone Zone name related to the response (or NULL).
  * \retval KNOT_EOK if passed.
  * \retval KNOT_ELIMIT when the limit is reached.
  */
 int rrl_query(rrl_table_t *rrl, const struct sockaddr_storage *a, rrl_req_t *req,
-              const struct zone *zone);
+              const knot_dname_t *zone);
 
 /*!
  * \brief Roll a dice whether answer slips or not.
@@ -187,5 +177,3 @@ int rrl_lock(rrl_table_t *rrl, int lk_id);
  * \retval KNOT_ERROR
  */
 int rrl_unlock(rrl_table_t *rrl, int lk_id);
-
-/*! @} */
diff --git a/src/knot/modules/rrl/rrl.c b/src/knot/modules/rrl/rrl.c
new file mode 100644
index 0000000000000000000000000000000000000000..445465a18e5a0fb9da51270c360e55ffd9ac9bc1
--- /dev/null
+++ b/src/knot/modules/rrl/rrl.c
@@ -0,0 +1,173 @@
+/*  Copyright (C) 2016 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "contrib/sockaddr.h"
+#include "contrib/mempattern.h"
+#include "knot/modules/rrl/rrl.h"
+#include "knot/modules/rrl/functions.h"
+
+/* Module configuration scheme. */
+#define MOD_RATE_LIMIT		"\x0A""rate-limit"
+#define MOD_SLIP		"\x04""slip"
+#define MOD_TBL_SIZE		"\x0A""table-size"
+#define MOD_WHITELIST		"\x09""whitelist"
+
+const yp_item_t scheme_mod_rrl[] = {
+	{ C_ID,           YP_TSTR,  YP_VNONE },
+	{ MOD_RATE_LIMIT, YP_TINT,  YP_VINT = { 1, INT32_MAX } },
+	{ MOD_SLIP,       YP_TINT,  YP_VINT = { 0, RRL_SLIP_MAX, 1 } },
+	{ MOD_TBL_SIZE,	  YP_TINT,  YP_VINT = { 1, INT32_MAX, 393241 } },
+	{ MOD_WHITELIST,  YP_TDATA, YP_VDATA = { 0, NULL, addr_range_to_bin,
+	                                         addr_range_to_txt }, YP_FMULTI },
+	{ C_COMMENT,      YP_TSTR,  YP_VNONE },
+	{ NULL }
+};
+
+int check_mod_rrl(conf_check_t *args)
+{
+	conf_val_t rl = conf_rawid_get_txn(args->conf, args->txn, C_MOD_RRL,
+	                                   MOD_RATE_LIMIT, args->id, args->id_len);
+	if (rl.code != KNOT_EOK) {
+		args->err_str = "no rate limit specified";
+		return KNOT_EINVAL;
+	}
+
+	return KNOT_EOK;
+}
+
+typedef struct {
+	mod_ctr_t *counters;
+	rrl_table_t *rrl;
+	int slip;
+	conf_val_t whitelist;
+} rrl_ctx_t;
+
+static int ratelimit_apply(int state, knot_pkt_t *pkt, struct query_data *qdata, void *ctx)
+{
+	assert(pkt && qdata && ctx);
+
+	rrl_ctx_t *context = ctx;
+
+	// Rate limit is not applied to TCP connections.
+	if (!(qdata->param->proc_flags & NS_QUERY_LIMIT_SIZE)) {
+		return state;
+	}
+
+	// Exempt clients.
+	if (conf_addr_range_match(&context->whitelist, qdata->param->remote)) {
+		return state;
+	}
+
+	rrl_req_t req = {
+		.w = pkt->wire,
+		.query = qdata->query
+	};
+
+	if (!EMPTY_LIST(qdata->wildcards)) {
+		req.flags = RRL_WILDCARD;
+	}
+
+	const knot_dname_t *zone_name = (qdata->zone != NULL) ? qdata->zone->name : NULL;
+
+	if (rrl_query(context->rrl, qdata->param->remote, &req, zone_name) == KNOT_EOK) {
+		// Rate limiting not applied.
+		return state;
+	}
+
+	if (context->slip > 0 && rrl_slip_roll(context->slip)) {
+		// Slip the answer.
+		mod_ctr_incr(context->counters, 0, 1);
+		qdata->err_truncated = true;
+		return KNOT_STATE_FAIL;
+	} else {
+		// Drop the answer.
+		mod_ctr_incr(context->counters, 1, 1);
+		pkt->size = 0;
+		return KNOT_STATE_DONE;
+	}
+}
+
+int rrl_load(struct query_plan *plan, struct query_module *self,
+             const knot_dname_t *zone)
+{
+	assert(self);
+
+	// Create RRL context.
+	rrl_ctx_t *ctx = mm_alloc(self->mm, sizeof(rrl_ctx_t));
+	if (ctx == NULL) {
+		return KNOT_ENOMEM;
+	}
+	memset(ctx, 0, sizeof(*ctx));
+
+	// Create table.
+	conf_val_t val = conf_mod_get(self->config, MOD_TBL_SIZE, self->id);
+	ctx->rrl = rrl_create(conf_int(&val));
+	if (ctx->rrl == NULL) {
+		mm_free(self->mm, ctx);
+		return KNOT_ENOMEM;
+	}
+
+	// Set locks.
+	int ret = rrl_setlocks(ctx->rrl, RRL_LOCK_GRANULARITY);
+	if (ret != KNOT_EOK) {
+		rrl_unload(self);
+		return ret;
+	}
+
+	// Set rate limit.
+	val = conf_mod_get(self->config, MOD_RATE_LIMIT, self->id);
+	ret = rrl_setrate(ctx->rrl, conf_int(&val));
+	if (ret != KNOT_EOK) {
+		rrl_unload(self);
+		return ret;
+	}
+
+	// Get whitelist.
+	val = conf_mod_get(self->config, MOD_WHITELIST, self->id);
+	ctx->whitelist = val;
+
+	// Get slip.
+	val = conf_mod_get(self->config, MOD_SLIP, self->id);
+	ctx->slip = conf_int(&val);
+
+	// Set up statistics counters.
+	ret = mod_stats_add(self, "slipped", 1, NULL);
+	if (ret != KNOT_EOK) {
+		rrl_unload(self);
+		return ret;
+	}
+
+	ret = mod_stats_add(self, "dropped", 1, NULL);
+	if (ret != KNOT_EOK) {
+		rrl_unload(self);
+		return ret;
+	}
+
+	ctx->counters = self->stats;
+	self->ctx = ctx;
+
+	return query_plan_step(plan, QPLAN_END, ratelimit_apply, self->ctx);
+}
+
+void rrl_unload(struct query_module *self)
+{
+	assert(self);
+
+	rrl_ctx_t *ctx = self->ctx;
+
+	rrl_destroy(ctx->rrl);
+	mm_free(self->mm, self->ctx);
+}
diff --git a/src/knot/modules/rrl/rrl.h b/src/knot/modules/rrl/rrl.h
new file mode 100644
index 0000000000000000000000000000000000000000..acb7590f840485374bdc0bdf0626692304d8d8a3
--- /dev/null
+++ b/src/knot/modules/rrl/rrl.h
@@ -0,0 +1,29 @@
+/*  Copyright (C) 2016 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/nameserver/query_module.h"
+
+/*! \brief Module scheme. */
+#define C_MOD_RRL "\x07""mod-rrl"
+extern const yp_item_t scheme_mod_rrl[];
+int check_mod_rrl(conf_check_t *args);
+
+/*! \brief Module interface. */
+int rrl_load(struct query_plan *plan, struct query_module *self,
+             const knot_dname_t *zone);
+void rrl_unload(struct query_module *self);
diff --git a/src/knot/nameserver/process_query.c b/src/knot/nameserver/process_query.c
index 29afa68effd63d00302e72a0a96f1bdc200fe47d..b3debdf37d636d1495f01c85b4ae8e0041fecd67 100644
--- a/src/knot/nameserver/process_query.c
+++ b/src/knot/nameserver/process_query.c
@@ -413,52 +413,6 @@ static int process_query_err(knot_layer_t *ctx, knot_pkt_t *pkt)
 	return KNOT_STATE_DONE;
 }
 
-/*!
- * \brief Apply rate limit.
- */
-static int ratelimit_apply(int state, knot_pkt_t *pkt, knot_layer_t *ctx)
-{
-	/* Check if rate limiting applies. */
-	struct query_data *qdata = QUERY_DATA(ctx);
-	server_t *server = qdata->param->server;
-	if (server->rrl == NULL) {
-		return state;
-	}
-
-	/* Exempt clients. */
-	conf_val_t *whitelist = &conf()->cache.srv_rate_limit_whitelist;
-	if (conf_addr_range_match(whitelist, qdata->param->remote)) {
-		return state;
-	}
-
-	rrl_req_t rrl_rq = {0};
-	rrl_rq.w = pkt->wire;
-	rrl_rq.query = qdata->query;
-	if (!EMPTY_LIST(qdata->wildcards)) {
-		rrl_rq.flags = RRL_WILDCARD;
-	}
-	if (rrl_query(server->rrl, qdata->param->remote,
-	              &rrl_rq, qdata->zone) == KNOT_EOK) {
-		/* Rate limiting not applied. */
-		return state;
-	}
-
-	/* Now it is slip or drop. */
-	int slip = conf()->cache.srv_rate_limit_slip;
-	if (slip > 0 && rrl_slip_roll(slip)) {
-		/* Answer slips. */
-		if (process_query_err(ctx, pkt) != KNOT_STATE_DONE) {
-			return KNOT_STATE_FAIL;
-		}
-		knot_wire_set_tc(pkt->wire);
-	} else {
-		/* Drop answer. */
-		pkt->size = 0;
-	}
-
-	return KNOT_STATE_DONE;
-}
-
 static int process_query_out(knot_layer_t *ctx, knot_pkt_t *pkt)
 {
 	assert(pkt && ctx);
@@ -558,11 +512,6 @@ finish:
 		}
 	}
 
-	/* Rate limits (if applicable). */
-	if (qdata->param->proc_flags & NS_QUERY_LIMIT_RATE) {
-		next_state = ratelimit_apply(next_state, pkt, ctx);
-	}
-
 	rcu_read_unlock();
 
 	return next_state;
diff --git a/src/knot/nameserver/process_query.h b/src/knot/nameserver/process_query.h
index 0698e6d7a4c8f4b6388533b334d6ac261560a268..401da20d2985f3fe49429e17a2dff9a3c12ce398 100644
--- a/src/knot/nameserver/process_query.h
+++ b/src/knot/nameserver/process_query.h
@@ -29,8 +29,7 @@ enum process_query_flag {
 	NS_QUERY_NO_AXFR    = 1 << 0, /* Don't process AXFR */
 	NS_QUERY_NO_IXFR    = 1 << 1, /* Don't process IXFR */
 	NS_QUERY_LIMIT_ANY  = 1 << 2, /* Limit ANY QTYPE (respond with TC=1) */
-	NS_QUERY_LIMIT_RATE = 1 << 3, /* Apply rate limits. */
-	NS_QUERY_LIMIT_SIZE = 1 << 4  /* Apply UDP size limit. */
+	NS_QUERY_LIMIT_SIZE = 1 << 3  /* Apply UDP size limit. */
 };
 
 /* Module load parameters. */
diff --git a/src/knot/nameserver/query_module.c b/src/knot/nameserver/query_module.c
index 8e0ca7d7c55a4b607d5569015f3c54bd0f85e715..67c67f5a8aeaacf8edd3bc1ccfd9b0bf5b144ba7 100644
--- a/src/knot/nameserver/query_module.c
+++ b/src/knot/nameserver/query_module.c
@@ -19,6 +19,7 @@
 #include "knot/nameserver/query_module.h"
 #include "contrib/mempattern.h"
 
+#include "knot/modules/rrl/rrl.h"
 #include "knot/modules/stats/stats.h"
 #include "knot/modules/synth_record/synth_record.h"
 #include "knot/modules/dnsproxy/dnsproxy.h"
@@ -34,6 +35,7 @@
 
 /*! \note All modules should be dynamically loaded later on. */
 static_module_t MODULES[] = {
+	{ C_MOD_RRL,          &rrl_load,          &rrl_unload,          MOD_SCOPE_ANY },
 	{ C_MOD_SYNTH_RECORD, &synth_record_load, &synth_record_unload, MOD_SCOPE_ANY },
 	{ C_MOD_DNSPROXY,     &dnsproxy_load,     &dnsproxy_unload,     MOD_SCOPE_ANY },
 	{ C_MOD_ONLINE_SIGN,  &online_sign_load,  &online_sign_unload,  MOD_SCOPE_ZONE, true },
diff --git a/src/knot/server/server.c b/src/knot/server/server.c
index c1bff958cdd6363c8704438353bf1e7cbe9e9e71..cf91649064c4f32b622334b951ec3c732efaa3f8 100644
--- a/src/knot/server/server.c
+++ b/src/knot/server/server.c
@@ -24,6 +24,7 @@
 #include "knot/common/log.h"
 #include "knot/common/stats.h"
 #include "knot/conf/confio.h"
+#include "knot/conf/migration.h"
 #include "knot/server/server.h"
 #include "knot/server/udp-handler.h"
 #include "knot/server/tcp-handler.h"
@@ -399,9 +400,6 @@ void server_deinit(server_t *server)
 	/* Free threads and event handlers. */
 	worker_pool_destroy(server->workers);
 
-	/* Free rate limits. */
-	rrl_destroy(server->rrl);
-
 	/* Free zone database. */
 	knot_zonedb_deep_free(&server->zone_db);
 
@@ -528,6 +526,12 @@ static int reload_conf(conf_t *new_conf)
 		log_info("reloading configuration database");
 	}
 
+	// Migrate from old schema.
+	int ret = conf_migrate(new_conf);
+	if (ret != KNOT_EOK) {
+		log_error("failed to migrate configuration (%s)", knot_strerror(ret));
+	}
+
 	/* Refresh hostname. */
 	conf_refresh_hostname(new_conf);
 
@@ -656,39 +660,6 @@ static int reconfigure_threads(conf_t *conf, server_t *server)
 	return reset_handler(server, IO_TCP, conf_tcp_threads(conf), tcp_master);
 }
 
-static int reconfigure_rate_limits(conf_t *conf, server_t *server)
-{
-	conf_val_t val = conf_get(conf, C_SRV, C_RATE_LIMIT);
-	int64_t rrl = conf_int(&val);
-
-	/* Rate limiting. */
-	if (!server->rrl && rrl > 0) {
-		val = conf_get(conf, C_SRV, C_RATE_LIMIT_TBL_SIZE);
-		server->rrl = rrl_create(conf_int(&val));
-		if (!server->rrl) {
-			log_error("failed to initialize rate limiting table");
-		} else {
-			rrl_setlocks(server->rrl, RRL_LOCK_GRANULARITY);
-		}
-	}
-	if (server->rrl) {
-		if (rrl_rate(server->rrl) != rrl) {
-			/* We cannot free it, threads may use it.
-			 * Setting it to <1 will disable rate limiting. */
-			if (rrl < 1) {
-				log_info("rate limiting, disabled");
-			} else {
-				log_info("rate limiting, enabled with %i responses/second",
-					 (int)rrl);
-			}
-			rrl_setrate(server->rrl, rrl);
-
-		} /* At this point, old buckets will converge to new rate. */
-	}
-
-	return KNOT_EOK;
-}
-
 void server_reconfigure(conf_t *conf, server_t *server)
 {
 	if (conf == NULL || server == NULL) {
@@ -700,14 +671,8 @@ void server_reconfigure(conf_t *conf, server_t *server)
 		log_info("Knot DNS %s starting", PACKAGE_VERSION);
 	}
 
-	/* Reconfigure rate limits. */
-	int ret;
-	if ((ret = reconfigure_rate_limits(conf, server)) < 0) {
-		log_error("failed to reconfigure rate limits (%s)",
-		          knot_strerror(ret));
-	}
-
 	/* Reconfigure server threads. */
+	int ret;
 	if ((ret = reconfigure_threads(conf, server)) < 0) {
 		log_error("failed to reconfigure server threads (%s)",
 		          knot_strerror(ret));
diff --git a/src/knot/server/server.h b/src/knot/server/server.h
index 284923591ac04aa8e67be859d2a599ff90f70c5d..4b155764673489ff3415d80df053fdac9936497b 100644
--- a/src/knot/server/server.h
+++ b/src/knot/server/server.h
@@ -34,7 +34,6 @@
 #include "knot/common/fdset.h"
 #include "knot/server/dthreads.h"
 #include "knot/common/ref.h"
-#include "knot/server/rrl.h"
 #include "knot/worker/pool.h"
 #include "knot/zone/zonedb.h"
 #include "contrib/ucw/lists.h"
@@ -110,10 +109,7 @@ typedef struct server {
 	evsched_t sched;
 
 	/*! \brief List of interfaces. */
-	ifacelist_t* ifaces;
-
-	/*! \brief Rate limiting. */
-	rrl_table_t *rrl;
+	ifacelist_t *ifaces;
 
 } server_t;
 
diff --git a/src/knot/server/udp-handler.c b/src/knot/server/udp-handler.c
index e789ec9db4ee2dd9f22b1e3cf534c0a901ea2c9a..16d0130a23b46c6234a9b5b0398d0e00c5df2db5 100644
--- a/src/knot/server/udp-handler.c
+++ b/src/knot/server/udp-handler.c
@@ -59,19 +59,15 @@ static void udp_handle(udp_context_t *udp, int fd, struct sockaddr_storage *ss,
                        struct iovec *rx, struct iovec *tx)
 {
 	/* Create query processing parameter. */
-	struct process_query_param param = {0};
-	param.remote = ss;
-	param.proc_flags  = NS_QUERY_NO_AXFR|NS_QUERY_NO_IXFR; /* No transfers. */
-	param.proc_flags |= NS_QUERY_LIMIT_SIZE; /* Enforce UDP packet size limit. */
-	param.proc_flags |= NS_QUERY_LIMIT_ANY;  /* Limit ANY over UDP (depends on zone as well). */
-	param.socket = fd;
-	param.server = udp->server;
-	param.thread_id = udp->thread_id;
-
-	/* Rate limit is applied? */
-	if (unlikely(udp->server->rrl != NULL) && udp->server->rrl->rate > 0) {
-		param.proc_flags |= NS_QUERY_LIMIT_RATE;
-	}
+	struct process_query_param param = {
+		.remote = ss,
+		.proc_flags = NS_QUERY_NO_AXFR | NS_QUERY_NO_IXFR | /* No transfers. */
+		              NS_QUERY_LIMIT_SIZE | /* Enforce UDP packet size limit. */
+		              NS_QUERY_LIMIT_ANY,  /* Limit ANY over UDP (depends on zone as well). */
+		.socket = fd,
+		.server = udp->server,
+		.thread_id = udp->thread_id
+	};
 
 	/* Start query processing. */
 	udp->layer.state = knot_layer_begin(&udp->layer, &param);
diff --git a/src/utils/knotd/main.c b/src/utils/knotd/main.c
index dd2bec909975e5e5735b408ed368dc47d9461b86..a8ebaaf471e659f24de5542e08a2345bfeaf086c 100644
--- a/src/utils/knotd/main.c
+++ b/src/utils/knotd/main.c
@@ -36,6 +36,7 @@
 #include "libknot/libknot.h"
 #include "knot/ctl/process.h"
 #include "knot/conf/conf.h"
+#include "knot/conf/migration.h"
 #include "knot/common/log.h"
 #include "knot/common/process.h"
 #include "knot/common/stats.h"
@@ -354,6 +355,12 @@ static int set_config(const char *confdb, const char *config)
 		}
 	}
 
+	// Migrate from old schema.
+	ret = conf_migrate(new_conf);
+	if (ret != KNOT_EOK) {
+		log_error("failed to migrate configuration (%s)", knot_strerror(ret));
+	}
+
 	/* Activate global query modules. */
 	conf_activate_modules(new_conf, NULL, &new_conf->query_modules,
 	                      &new_conf->query_plan);
diff --git a/tests-extra/tests/modules/rrl/test.py b/tests-extra/tests/modules/rrl/test.py
new file mode 100644
index 0000000000000000000000000000000000000000..a79f02e9f513e17a307f1189cd1a1296350c260e
--- /dev/null
+++ b/tests-extra/tests/modules/rrl/test.py
@@ -0,0 +1,203 @@
+#!/usr/bin/env python3
+
+'''RRL module functionality test'''
+
+import dns.exception
+import dns.message
+import dns.query
+import os
+import time
+
+from dnstest.libknot import libknot
+from dnstest.test import Test
+from dnstest.module import ModRRL
+from dnstest.utils import *
+
+ctl = libknot.control.KnotCtl()
+
+t = Test(stress=False)
+
+ModRRL.check()
+
+knot = t.server("knot")
+zones = t.zone_rnd(2, dnssec=False, records=1)
+t.link(zones, knot)
+
+def send_queries(server, name, run_time=1.0, query_time=0.05):
+    """
+    Send UDP queries to the server for certain time and get replies statistics.
+    """
+    replied, slipped, dropped = 0, 0, 0
+    start = time.time()
+    while time.time() < start + run_time:
+        try:
+            query = dns.message.make_query(name, "SOA", want_dnssec=False)
+            response = dns.query.udp(query, server.addr, port=server.port, timeout=query_time)
+        except dns.exception.Timeout:
+            response = None
+
+        if response is None:
+            dropped += 1
+        elif response.flags & dns.flags.TC:
+            slipped += 1
+        else:
+            replied += 1
+
+    return dict(replied=replied, slipped=slipped, dropped=dropped)
+
+def check_result(name, res, rate=0, slip=None):
+    """
+    Check response result.
+
+    We cannot send queries in parallel. And we have to give the server some time
+    to respond, especially under valgrind. Therefore we have to be tolerant when
+    counting responses when packets are being dropped.
+    """
+    detail_log("RRL %s" % name)
+    detail_log(", ".join(["%s %d" % (s, res[s]) for s in ["replied", "slipped", "dropped"]]))
+
+    ok = False
+
+    if rate == 0:
+        ok = res["replied"] >= 100 and res["slipped"] == 0 and res["dropped"] == 0
+    elif slip == 0:
+        ok = res["replied"] > 0 and res["replied"] < 100 and \
+             res["slipped"] == 0 and res["dropped"] >= 5
+    elif slip == 1:
+        ok = res["replied"] > 0 and res["replied"] < 100 and \
+             res["slipped"] >= 100 and res["dropped"] == 0
+    else:
+        ok = res["replied"] > 0 and res["replied"] < 100 and \
+             res["slipped"] >= 5 and res["dropped"] >= 5
+
+    if ok:
+        detail_log("success")
+    else:
+        detail_log("error")
+        set_err("RRL ERROR")
+
+def cmp_stats(server, res, zone_name=None):
+    try:
+        ctl = libknot.control.KnotCtl()
+        ctl.connect(os.path.join(server.dir, "knot.sock"))
+
+        if zone_name:
+            ctl.send_block(cmd="zone-stats", section="mod-rrl", zone=zone_name, flags="F")
+        else:
+            ctl.send_block(cmd="stats", section="mod-rrl", flags="F")
+
+        stats = ctl.receive_stats()
+        detail_log(stats)
+
+        if zone_name:
+            ok = int(stats["zone"][zone_name.lower()]["mod-rrl"]["dropped"]) == res["dropped"] and \
+                 int(stats["zone"][zone_name.lower()]["mod-rrl"]["slipped"]) == res["slipped"]
+        else:
+            ok = int(stats["mod-rrl"]["dropped"]) == res["dropped"] and \
+                 int(stats["mod-rrl"]["slipped"]) == res["slipped"]
+
+        if ok:
+            detail_log("stats success")
+        else:
+            detail_log("stats error")
+            set_err("RRL STATS ERROR")
+
+    finally:
+        ctl.send(libknot.control.KnotCtlType.END)
+        ctl.close()
+
+def reconfigure(server, zone, rate_limit, slip, whitelist=None):
+    """
+    Reconfigure server module.
+    """
+    server.clear_modules(None)
+    server.clear_modules(zone)
+    server.add_module(zone, ModRRL(rate_limit=rate_limit, slip=slip, whitelist=whitelist))
+    server.gen_confile()
+    server.reload()
+
+t.start()
+
+knot.zones_wait(zones)
+
+### RRL global module
+
+# Disabled
+res = send_queries(knot, zones[0].name)
+check_result("disabled", res)
+
+# All drop
+reconfigure(knot, None, 5, 0)
+res = send_queries(knot, zones[0].name)
+check_result("global, zone 1, all drop", res, 5, 0)
+cmp_stats(knot, res)
+time.sleep(2)
+res = send_queries(knot, zones[1].name)
+check_result("global, zone 2, all drop", res, 5, 0)
+
+# All slip
+reconfigure(knot, None, 5, 1)
+res = send_queries(knot, zones[0].name)
+check_result("global, zone 1, all slip", res, 5, 1)
+cmp_stats(knot, res)
+time.sleep(2)
+res = send_queries(knot, zones[1].name)
+check_result("global, zone 2, all slip", res, 5, 1)
+
+# 50% slip
+reconfigure(knot, None, 5, 2)
+res = send_queries(knot, zones[0].name)
+check_result("global, zone 1, 50% slip", res, 5, 2)
+cmp_stats(knot, res)
+time.sleep(2)
+res = send_queries(knot, zones[1].name)
+check_result("global, zone 2, 50% slip", res, 5, 2)
+
+# Whitelist
+reconfigure(knot, None, 5, 0, whitelist=knot.addr)
+res = send_queries(knot, zones[0].name)
+cmp_stats(knot, res)
+check_result("global, zone 1, whitelist", res, 0)
+time.sleep(2)
+res = send_queries(knot, zones[1].name)
+check_result("global, zone 2, whitelist", res, 0)
+
+### RRL zone module
+
+# All drop
+reconfigure(knot, zones[0], 5, 0)
+res = send_queries(knot, zones[0].name)
+check_result("zone 1, all drop", res, 5, 0)
+cmp_stats(knot, res, zones[0].name)
+time.sleep(2)
+res = send_queries(knot, zones[1].name)
+check_result("zone 2, disabled", res)
+
+# All slip
+reconfigure(knot, zones[0], 5, 1)
+res = send_queries(knot, zones[0].name)
+check_result("zone 1, all slip", res, 5, 1)
+cmp_stats(knot, res, zones[0].name)
+time.sleep(2)
+res = send_queries(knot, zones[1].name)
+check_result("zone 2, disabled", res)
+
+# 50% slip
+reconfigure(knot, zones[0], 5, 2)
+res = send_queries(knot, zones[0].name)
+check_result("zone 1, 50% slip", res, 5, 2)
+cmp_stats(knot, res, zones[0].name)
+time.sleep(2)
+res = send_queries(knot, zones[1].name)
+check_result("zone 2, disabled", res)
+
+# Whitelist
+reconfigure(knot, zones[0], 5, 0, whitelist=knot.addr)
+res = send_queries(knot, zones[0].name)
+check_result("zone 1, whitelist", res, 0)
+cmp_stats(knot, res, zones[0].name)
+time.sleep(2)
+res = send_queries(knot, zones[1].name)
+check_result("zone 2, disabled", res)
+
+t.end()
diff --git a/tests-extra/tools/dnstest/module.py b/tests-extra/tools/dnstest/module.py
index 744ed25f1fbf1b5052d9cbff251fedc37347fd42..4673e485918758c2ac155e7c903682e7931c9531 100644
--- a/tests-extra/tools/dnstest/module.py
+++ b/tests-extra/tools/dnstest/module.py
@@ -109,6 +109,36 @@ class ModDnstap(KnotModule):
 
         return conf
 
+class ModRRL(KnotModule):
+    '''RRL module'''
+
+    src_name = "rrl_load"
+    conf_name = "mod-rrl"
+
+    def __init__(self, rate_limit, slip=None, table_size=None, whitelist=None):
+        super().__init__()
+        self.rate_limit = rate_limit
+        self.slip = slip
+        self.table_size = table_size
+        self.whitelist = whitelist
+
+    def get_conf(self, conf=None):
+        if not conf:
+            conf = dnstest.config.KnotConf()
+
+        conf.begin(self.conf_name)
+        conf.id_item("id", self.conf_id)
+        conf.item_str("rate-limit", self.rate_limit)
+        if self.slip or self.slip == 0:
+            conf.item_str("slip", self.slip)
+        if self.table_size:
+            conf.item_str("table-size", self.table_size)
+        if self.whitelist:
+            conf.item_str("whitelist", self.whitelist)
+        conf.end()
+
+        return conf
+
 class ModDnsproxy(KnotModule):
     '''Dnsproxy module'''
 
diff --git a/tests-extra/tools/dnstest/server.py b/tests-extra/tools/dnstest/server.py
index a84975c365ec4c775aec306fe121f75a7dd1fbee..57e9188ff800598fc8fbb77d6cd55c5d23c07909 100644
--- a/tests-extra/tools/dnstest/server.py
+++ b/tests-extra/tools/dnstest/server.py
@@ -64,6 +64,9 @@ class Zone(object):
     def add_module(self, module):
         self.modules.append(module)
 
+    def clear_modules(self):
+        self.modules.clear()
+
     def disable_master(self, new_zone_file):
         self.zfile.remove()
         self.zfile = new_zone_file
@@ -661,6 +664,14 @@ class Server(object):
         else:
             self.modules.append(module)
 
+    def clear_modules(self, zone):
+        zone = zone_arg_check(zone)
+
+        if zone:
+            self.zones[zone.name].clear_modules()
+        else:
+            self.modules.clear()
+
     def clean(self, zone=True, timers=True):
         if zone:
             zone = zone_arg_check(zone)
diff --git a/tests/.gitignore b/tests/.gitignore
index 3d3af24a514ba319b52079411f0240a3f9b3f121..49a3af1bb3c6f4286ed4a81ff11fcf510fd1d403 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -46,12 +46,12 @@
 /fdset
 /journal
 /modules/online_sign
+/modules/rrl
 /node
 /process_answer
 /process_query
 /query_module
 /requestor
-/rrl
 /semantic_check
 /server
 /utils/test_cert
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 5e08bdb9e44a3c7955d088c7a5d30eec26586aa9..a3bc0277d3fe3e4155ae88680b15901d4f07e9de 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -49,6 +49,7 @@ check_PROGRAMS += \
 
 check_PROGRAMS += \
 	modules/online_sign		\
+	modules/rrl			\
 	utils/test_cert			\
 	utils/test_lookup		\
 	acl				\
@@ -65,7 +66,6 @@ check_PROGRAMS += \
 	process_query			\
 	query_module			\
 	requestor			\
-	rrl				\
 	server				\
 	worker_pool			\
 	worker_queue			\
diff --git a/tests/confio.c b/tests/confio.c
index 175765e6a38e81a23b72586aa50c5b6ec9581c61..e4f0b8e4cfb4bbbdaf4f1153fae126431f4fe9d1 100644
--- a/tests/confio.c
+++ b/tests/confio.c
@@ -365,7 +365,7 @@ static void test_conf_io_set(void)
 	   KNOT_YP_EINVAL_ITEM, "set unknown key1");
 	ok(conf_io_set("include", NULL, NULL, NULL) ==
 	   KNOT_YP_ENODATA, "set non-group without data");
-	ok(conf_io_set("server", "rate-limit", NULL, "x") ==
+	ok(conf_io_set("server", "background-workers", NULL, "x") ==
 	   KNOT_EINVAL, "set invalid data");
 
 	// ERR callback
@@ -459,7 +459,7 @@ static void test_conf_io_unset(void)
 	   KNOT_YP_EINVAL_ITEM, "unset unknown key1");
 	ok(conf_io_unset("include", NULL, NULL, "file") ==
 	   KNOT_ENOTSUP, "unset non-group item");
-	ok(conf_io_unset("server", "rate-limit", NULL, "x") ==
+	ok(conf_io_unset("server", "background-workers", NULL, "x") ==
 	   KNOT_EINVAL, "unset invalid data");
 
 	// Single group, single value.
@@ -885,7 +885,7 @@ static void test_conf_io_list(void)
 	ok(conf_io_list("server", &io) ==
 	   KNOT_EOK, "list group");
 	ref = "server.version\n"
-	      "server.rate-limit\n"
+	      "server.background-workers\n"
 	      "server.listen\n"
 	      "server.tcp-handshake-timeout\n"
 	      "server.tcp-idle-timeout\n"
@@ -893,14 +893,13 @@ static void test_conf_io_list(void)
 	      "server.max-tcp-clients\n"
 	      "server.max-udp-payload\n"
 	      "server.max-ipv4-udp-payload\n"
-	      "server.max-ipv6-udp-payload\n"
-	      "server.rate-limit-slip";
+	      "server.max-ipv6-udp-payload";
 	ok(strcmp(ref, out) == 0, "compare result");
 }
 
 static const yp_item_t desc_server[] = {
 	{ C_VERSION,              YP_TSTR,  YP_VNONE },
-	{ C_RATE_LIMIT,           YP_TINT,  YP_VNONE },
+	{ C_BG_WORKERS,           YP_TINT,  YP_VNONE },
 	{ C_LISTEN,               YP_TADDR, YP_VNONE, YP_FMULTI },
 	// Required config cache items - assert fix.
 	{ C_TCP_HSHAKE_TIMEOUT,   YP_TINT,  YP_VNONE },
@@ -910,7 +909,6 @@ static const yp_item_t desc_server[] = {
 	{ C_MAX_UDP_PAYLOAD,      YP_TINT,  YP_VNONE },
 	{ C_MAX_IPV4_UDP_PAYLOAD, YP_TINT,  YP_VNONE },
 	{ C_MAX_IPV6_UDP_PAYLOAD, YP_TINT,  YP_VNONE },
-	{ C_RATE_LIMIT_SLIP,	  YP_TINT,  YP_VNONE },
 	{ NULL }
 };
 
diff --git a/tests/rrl.c b/tests/modules/rrl.c
similarity index 91%
rename from tests/rrl.c
rename to tests/modules/rrl.c
index e96d7b9504fec7cca14f0e69b891678300621ff0..c128d51e186fc53970241c8eaeaf4366cdaa459e 100644
--- a/tests/rrl.c
+++ b/tests/modules/rrl.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2011 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2016 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -14,17 +14,13 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#include <sys/types.h>
-#include <sys/socket.h>
 #include <tap/basic.h>
 
 #include "dnssec/crypto.h"
 #include "dnssec/random.h"
-#include "knot/conf/conf.h"
-#include "knot/server/rrl.h"
-#include "knot/zone/zone.h"
-#include "libknot/descriptor.h"
+#include "libknot/libknot.h"
 #include "contrib/sockaddr.h"
+#include "knot/modules/rrl/functions.h"
 
 /* Enable time-dependent tests. */
 //#define ENABLE_TIMED_TESTS
@@ -50,12 +46,12 @@ struct runnable_data {
 	rrl_table_t *rrl;
 	struct sockaddr_storage *addr;
 	rrl_req_t *rq;
-	zone_t *zone;
+	knot_dname_t *zone;
 };
 
 static void* rrl_runnable(void *arg)
 {
-	struct runnable_data* d = (struct runnable_data*)arg;
+	struct runnable_data *d = (struct runnable_data *)arg;
 	struct sockaddr_storage addr;
 	memcpy(&addr, d->addr, sizeof(struct sockaddr_storage));
 	int lock = -1;
@@ -143,9 +139,7 @@ int main(int argc, char *argv[])
 	is_int(KNOT_EOK, ret, "rrl: setlocks");
 
 	/* 4. N unlimited requests. */
-	knot_dname_t *zone_name = knot_dname_from_str_alloc("rrl.");
-	zone_t *zone = zone_new(zone_name);
-	knot_dname_free(&zone_name, NULL);
+	knot_dname_t *zone = knot_dname_from_str_alloc("rrl.");
 
 	struct sockaddr_storage addr;
 	struct sockaddr_storage addr6;
@@ -199,7 +193,7 @@ int main(int argc, char *argv[])
 	ok(rd.passed, "rrl: hashtable is ~ consistent");
 #endif
 
-	zone_free(&zone);
+	knot_dname_free(&zone, NULL);
 	knot_pkt_free(&query);
 	rrl_destroy(rrl);
 	dnssec_crypto_cleanup();