diff --git a/pdm.lock b/pdm.lock
index 8dd8de3..75291bf 100644
--- a/pdm.lock
+++ b/pdm.lock
@@ -5,7 +5,7 @@
 groups = ["default", "dev"]
 strategy = ["cross_platform"]
 lock_version = "4.4.1"
-content_hash = "sha256:a1d2e245c8350af379963adad03a3a7d34992b3baf4f5302faf3588b6dc977fc"
+content_hash = "sha256:ed8c7d415898ea8f15e65023b6c8d43be31aef16a0536d278f33bfa43911c699"
 
 [[package]]
 name = "bandit"
@@ -25,7 +25,7 @@ files = [
 
 [[package]]
 name = "black"
-version = "24.2.0"
+version = "24.3.0"
 requires_python = ">=3.8"
 summary = "The uncompromising code formatter."
 dependencies = [
@@ -38,28 +38,28 @@ dependencies = [
     "typing-extensions>=4.0.1; python_version < \"3.11\"",
 ]
 files = [
-    {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"},
-    {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"},
-    {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"},
-    {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"},
-    {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"},
-    {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"},
-    {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"},
-    {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"},
-    {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"},
-    {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"},
-    {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"},
-    {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"},
-    {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"},
-    {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"},
-    {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"},
-    {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"},
-    {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"},
-    {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"},
-    {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"},
-    {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"},
-    {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"},
-    {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"},
+    {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"},
+    {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"},
+    {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"},
+    {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"},
+    {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"},
+    {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"},
+    {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"},
+    {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"},
+    {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"},
+    {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"},
+    {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"},
+    {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"},
+    {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"},
+    {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"},
+    {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"},
+    {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"},
+    {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"},
+    {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"},
+    {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"},
+    {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"},
+    {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"},
+    {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"},
 ]
 
 [[package]]
@@ -1160,6 +1160,7 @@ files = [
     {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
     {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
     {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
     {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
     {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
     {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
diff --git a/pyproject.toml b/pyproject.toml
index 49d15cd..071a2d4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,7 +29,7 @@ license = {text = "MIT"}
 dev = [
     "pytest>=7.3.1",
     "coverage>=7.2.7",
-    "black>=23.3.0",
+    "black>=24.3.0",
     "pytest-cov>=4.1.0",
     "pre-commit>=3.3.2",
     "mypy>=1.3.0",
diff --git a/requirements.txt b/requirements.txt
index da92629..533f74b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,29 +4,29 @@
 bandit==1.7.8 \
     --hash=sha256:36de50f720856ab24a24dbaa5fee2c66050ed97c1477e0a1159deab1775eab6b \
     --hash=sha256:509f7af645bc0cd8fd4587abc1a038fc795636671ee8204d502b933aee44f381
-black==24.2.0 \
-    --hash=sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8 \
-    --hash=sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8 \
-    --hash=sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd \
-    --hash=sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9 \
-    --hash=sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31 \
-    --hash=sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92 \
-    --hash=sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f \
-    --hash=sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29 \
-    --hash=sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4 \
-    --hash=sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693 \
-    --hash=sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218 \
-    --hash=sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a \
-    --hash=sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23 \
-    --hash=sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0 \
-    --hash=sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982 \
-    --hash=sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894 \
-    --hash=sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540 \
-    --hash=sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430 \
-    --hash=sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b \
-    --hash=sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2 \
-    --hash=sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6 \
-    --hash=sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d
+black==24.3.0 \
+    --hash=sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f \
+    --hash=sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93 \
+    --hash=sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11 \
+    --hash=sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0 \
+    --hash=sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9 \
+    --hash=sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5 \
+    --hash=sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213 \
+    --hash=sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d \
+    --hash=sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7 \
+    --hash=sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837 \
+    --hash=sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f \
+    --hash=sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395 \
+    --hash=sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995 \
+    --hash=sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f \
+    --hash=sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597 \
+    --hash=sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959 \
+    --hash=sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5 \
+    --hash=sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb \
+    --hash=sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4 \
+    --hash=sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7 \
+    --hash=sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd \
+    --hash=sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7
 boolean-py==4.0 \
     --hash=sha256:17b9a181630e43dde1851d42bef546d616d5d9b4480357514597e78b203d06e4 \
     --hash=sha256:2876f2051d7d6394a531d82dc6eb407faa0b01a0a0b3083817ccd7323b8d96bd
diff --git a/src/pinnwand/defensive.py b/src/pinnwand/defensive.py
index 0b64d53..4a5ebd5 100644
--- a/src/pinnwand/defensive.py
+++ b/src/pinnwand/defensive.py
@@ -1,6 +1,5 @@
-import ipaddress
 import re
-from typing import Dict, Union
+from typing import Dict
 from functools import wraps
 
 import token_bucket
@@ -13,18 +12,10 @@ from pinnwand.configuration import Configuration, ConfigurationProvider
 log = logger.get_logger(__name__)
 
 
-ratelimit_area: Dict[
-    str,
-    Dict[
-        Union[ipaddress.IPv4Address, ipaddress.IPv6Address],
-        token_bucket.Limiter,
-    ],
-] = {}
+ratelimit_area: Dict[str, token_bucket.Limiter] = {}
 
 
-def should_be_ratelimited(
-    request: HTTPServerRequest, area: str = "global"
-) -> bool:
+def should_be_ratelimited(ip_address: str, area: str = "global") -> bool:
     """Test if a requesting IP is ratelimited for a certain area. Areas are
     different functionalities of the website, for example 'view' or 'input' to
     differentiate between creating new pastes (low volume) or high volume
@@ -38,20 +29,17 @@ def should_be_ratelimited(
     configuration: Configuration = ConfigurationProvider.get_config()
 
     if area not in ratelimit_area:
-        ratelimit_area[area] = {}
-
-    # TODO handle valueerror as validationerror?
-    address = ipaddress.ip_address(str(request.remote_ip))
-
-    if address not in ratelimit_area[area]:
-        ratelimit_area[area][address] = token_bucket.Limiter(
+        ratelimit_area[area] = token_bucket.Limiter(
             configuration.ratelimit[area]["refill"],
             configuration.ratelimit[area]["capacity"],
             token_bucket.MemoryStorage(),
         )
 
-    if not ratelimit_area[area][address].consume(1):
-        log.warning("%s hit rate limit for %r", address, area)
+    if not ratelimit_area[area].consume(
+        ip_address.encode("utf-8"),
+        configuration.ratelimit[area]["consume"],
+    ):
+        log.warning("%s hit rate limit for %r", ip_address, area)
         return True
 
     return False
@@ -63,7 +51,7 @@ def ratelimit(area: str):
     def wrapper(func):
         @wraps(func)
         def inner(request_handler: RequestHandler, *args, **kwargs):
-            if should_be_ratelimited(request_handler.request, area):
+            if should_be_ratelimited(request_handler.request.remote_ip, area):
                 raise error.RatelimitError()
             return func(request_handler, *args, **kwargs)
 
diff --git a/src/pinnwand/handler/api_curl.py b/src/pinnwand/handler/api_curl.py
index 1fca553..1cf569e 100644
--- a/src/pinnwand/handler/api_curl.py
+++ b/src/pinnwand/handler/api_curl.py
@@ -31,7 +31,6 @@ class Create(tornado.web.RequestHandler):
 
     @defensive.ratelimit(area="create")
     def post(self) -> None:
-
         configuration: Configuration = ConfigurationProvider.get_config()
         lexer = self.get_body_argument("lexer", "text")
         raw = self.get_body_argument("raw", "", strip=False)

diff --git a/test/integration/test_http_api.py b/test/integration/test_http_api.py
index 47b6dd5..672e8ac 100644
--- a/test/integration/test_http_api.py
+++ b/test/integration/test_http_api.py
@@ -1,20 +1,23 @@
 import json
+import copy
 import urllib.parse
-
+import unittest.mock
 import tornado.testing
 import tornado.web
 
 from pinnwand.configuration import Configuration, ConfigurationProvider
+from pinnwand import app, utility
+from pinnwand.database import manager, utils as database_utils
+
 configuration: Configuration = ConfigurationProvider.get_config()
 
-configuration._ratelimit["read"]["capacity"] = 2**64 - 1
-configuration._ratelimit["create"]["capacity"] = 2**64 - 1
-configuration._ratelimit["delete"]["capacity"] = 2**64 - 1
 
-from pinnwand import app, utility
-from pinnwand.database import manager, utils as database_utils
+ratelimit_copy = copy.deepcopy(configuration._ratelimit)
+for area in ("read", "create", "delete"):
+    ratelimit_copy[area]["capacity"] = 2**64 - 1
 
 
+@unittest.mock.patch.dict(configuration._ratelimit, ratelimit_copy)
 class DeprecatedAPITestCase(tornado.testing.AsyncHTTPTestCase):
     def setUp(self) -> None:
         super().setUp()
@@ -117,7 +120,6 @@ class DeprecatedAPITestCase(tornado.testing.AsyncHTTPTestCase):
             ),
         )
 
-        print(response.body)
         assert response.code == 200
 
     def test_api_new_large_file(self) -> None:
diff --git a/test/integration/test_http_curl.py b/test/integration/test_http_curl.py
index d648b74..489e23d 100644
--- a/test/integration/test_http_curl.py
+++ b/test/integration/test_http_curl.py
@@ -1,20 +1,22 @@
 import re
 import urllib.parse
-
+import unittest.mock
 import tornado.testing
 import tornado.web
-
+import copy
 from pinnwand.configuration import Configuration, ConfigurationProvider
-configuration: Configuration = ConfigurationProvider.get_config()
-
-configuration._ratelimit["read"]["capacity"] = 2**64 - 1
-configuration._ratelimit["create"]["capacity"] = 2**64 - 1
-configuration._ratelimit["delete"]["capacity"] = 2**64 - 1
 
 from pinnwand import app
 from pinnwand.database import manager, utils as database_utils
 
+configuration: Configuration = ConfigurationProvider.get_config()
 
+ratelimit_copy = copy.deepcopy(configuration._ratelimit)
+for area in ("read", "create", "delete"):
+    ratelimit_copy[area]["capacity"] = 2**64 - 1
+
+
+@unittest.mock.patch.dict(configuration._ratelimit, ratelimit_copy)
 class CurlTestCase(tornado.testing.AsyncHTTPTestCase):
     def setUp(self) -> None:
         super().setUp()
@@ -131,7 +133,6 @@ class CurlTestCase(tornado.testing.AsyncHTTPTestCase):
             .group(1)  # type: ignore
             .decode("ascii")
         )
-        print(paste)
         paste = urllib.parse.urlparse(paste).path
 
         response = self.fetch(
@@ -157,8 +158,6 @@ class CurlTestCase(tornado.testing.AsyncHTTPTestCase):
         )
         paste = urllib.parse.urlparse(paste).path
 
-        print(repr(paste))
-
         response = self.fetch(
             paste,
             method="GET",
@@ -262,8 +261,6 @@ class CurlTestCase(tornado.testing.AsyncHTTPTestCase):
             follow_redirects=False,
         )
 
-        print(response.body)
-
         paste = (
             re.search(b"Paste URL:   (.*)", response.body)
             .group(1)  # type: ignore
diff --git a/test/integration/test_http_ratelimit.py b/test/integration/test_http_ratelimit.py
new file mode 100644
index 0000000..ce3e60b
--- /dev/null
+++ b/test/integration/test_http_ratelimit.py
@@ -0,0 +1,90 @@
+import copy
+import time
+import unittest.mock
+
+import tornado.testing
+import tornado.web
+
+from pinnwand import app, configuration, defensive
+
+
+class RateLimitTestCase(tornado.testing.AsyncHTTPTestCase):
+
+    def get_app(self) -> tornado.web.Application:
+        return app.make_application()
+
+    def test_ratelimit_verification_on_endpoints(self):
+        with unittest.mock.patch("pinnwand.defensive.should_be_ratelimited") as patch:
+            patch.return_value = False
+
+            self.fetch(
+                "/",
+                method="GET",
+            )
+
+            patch.assert_called()
+            patch.reset_mock()
+
+    def test_ratelimit_application_on_one_client(self):
+        config = configuration.ConfigurationProvider.get_config()
+        ratelimlit_copy = copy.deepcopy(config._ratelimit)
+        ratelimlit_copy["read"]["capacity"] = 2
+        ratelimlit_copy["read"]["consume"] = 2
+        ratelimlit_copy["read"]["refill"] = 1
+
+        with unittest.mock.patch.dict("pinnwand.defensive.ConfigurationProvider._config._ratelimit", ratelimlit_copy):
+            with unittest.mock.patch.dict("pinnwand.defensive.ratelimit_area", clear=True):
+                response = self.fetch(
+                    "/",
+                    method="GET",
+                )
+
+                assert response.code == 200
+
+                response = self.fetch(
+                    "/",
+                    method="GET",
+                )
+
+                assert response.code == 429
+
+    def test_ratelimit_application_on_multiple_clients(self):
+        config = configuration.ConfigurationProvider.get_config()
+        ratelimlit_copy = copy.deepcopy(config._ratelimit)
+        area = "read"
+        ratelimlit_copy[area]["capacity"] = 10
+        ratelimlit_copy[area]["consume"] = 7
+        ratelimlit_copy[area]["refill"] = 1
+
+        ip1 = "192.168.15.32"
+        ip2 = "10.45.134.23"
+
+        with unittest.mock.patch.dict("pinnwand.defensive.ConfigurationProvider._config._ratelimit", ratelimlit_copy):
+            with unittest.mock.patch.dict("pinnwand.defensive.ratelimit_area", clear=True):
+                assert defensive.should_be_ratelimited(ip1, area) is False
+                assert defensive.should_be_ratelimited(ip1, area) is True
+                assert defensive.should_be_ratelimited(ip2, area) is False
+                assert defensive.should_be_ratelimited(ip2, area) is True
+                assert defensive.should_be_ratelimited(ip2, area) is True
+                time.sleep(10)  # Give it enough time to replenish
+                assert defensive.should_be_ratelimited(ip1, area) is False
+                assert defensive.should_be_ratelimited(ip2, area) is False
+
+    def test_bucket_tokens_consumption(self):
+        config = configuration.ConfigurationProvider.get_config()
+        ratelimlit_copy = copy.deepcopy(config._ratelimit)
+        area = "read"
+        consumption = 7
+        capacity = 10
+        ratelimlit_copy[area]["capacity"] = capacity
+        ratelimlit_copy[area]["consume"] = consumption
+        ratelimlit_copy[area]["refill"] = 1
+
+        ip = "192.168.15.32"
+        with unittest.mock.patch.dict("pinnwand.defensive.ConfigurationProvider._config._ratelimit", ratelimlit_copy):
+            with unittest.mock.patch.dict("pinnwand.defensive.ratelimit_area", clear=True):
+                defensive.should_be_ratelimited(ip, area)
+                limiter = defensive.ratelimit_area[area]
+                tokens_remaining = limiter._storage.get_token_count(ip.encode("utf-8"))
+                assert tokens_remaining == capacity - consumption
+
diff --git a/test/integration/test_http_website.py b/test/integration/test_http_website.py
index cbe19ea..630bd26 100644
--- a/test/integration/test_http_website.py
+++ b/test/integration/test_http_website.py
@@ -1,20 +1,22 @@
 import urllib.parse
-
+import unittest.mock
 import tornado.testing
 import tornado.web
-
+import copy
 
 from pinnwand.configuration import Configuration, ConfigurationProvider
+from pinnwand import app
+from pinnwand.database import manager, utils as database_utils
+
 configuration: Configuration = ConfigurationProvider.get_config()
 
-configuration._ratelimit["read"]["capacity"] = 2**64 - 1
-configuration._ratelimit["create"]["capacity"] = 2**64 - 1
-configuration._ratelimit["delete"]["capacity"] = 2**64 - 1
 
-from pinnwand import app
-from pinnwand.database import manager, utils as database_utils
+ratelimit_copy = copy.deepcopy(configuration._ratelimit)
+for area in ("read", "create", "delete"):
+    ratelimit_copy[area]["capacity"] = 2**64 - 1
 
 
+@unittest.mock.patch.dict(configuration._ratelimit, ratelimit_copy)
 class WebsiteTestCase(tornado.testing.AsyncHTTPTestCase):
     def setUp(self) -> None:
         super().setUp()

Fix consume ratelimit config
Configuration for rate limits lets you set the consume rate:
https://github.com/supakeen/pinnwand/blob/e737aba402548f83bc458c62ac6e10e69419f2b1/src/pinnwand/configuration.py#L12-L26

However, this configuration is not used, instead the consume value is always 1:
https://github.com/supakeen/pinnwand/blob/e737aba402548f83bc458c62ac6e10e69419f2b1/src/pinnwand/defensive.py#L46
test/integration/test_http_ratelimit.py::RateLimitTestCase::test_bucket_tokens_consumption test/integration/test_http_ratelimit.py::RateLimitTestCase::test_ratelimit_application_on_multiple_clients test/integration/test_http_ratelimit.py::RateLimitTestCase::test_ratelimit_application_on_one_client
test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_get_expiries test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_get_lexers test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_new test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_new_empty_code test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_new_invalid_lexer test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_new_large_file test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_new_no_code test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_new_no_expiry test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_new_no_lexer test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_new_small_file test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_new_space_code test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_new_wrong_method test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_remove test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_return_filename test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_show test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_show_nonexistent test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_show_spaced test/integration/test_http_api.py::DeprecatedAPITestCase::test_api_show_web test/integration/test_http_api.py::APIv1TestCase::test_api_detail_many_files test/integration/test_http_api.py::APIv1TestCase::test_api_new test/integration/test_http_api.py::APIv1TestCase::test_api_new_invalid_body test/integration/test_http_api.py::APIv1TestCase::test_api_new_invalid_lexer test/integration/test_http_api.py::APIv1TestCase::test_api_new_large_file test/integration/test_http_api.py::APIv1TestCase::test_api_new_many_file test/integration/test_http_api.py::APIv1TestCase::test_api_new_many_file_large test/integration/test_http_api.py::APIv1TestCase::test_api_new_no_content test/integration/test_http_api.py::APIv1TestCase::test_api_new_no_expiry test/integration/test_http_api.py::APIv1TestCase::test_api_new_no_files test/integration/test_http_api.py::APIv1TestCase::test_api_new_no_lexer test/integration/test_http_api.py::APIv1TestCase::test_api_new_small_file test/integration/test_http_api.py::APIv1TestCase::test_api_new_wrong_method test/integration/test_http_curl.py::CurlTestCase::test_curl_post test/integration/test_http_curl.py::CurlTestCase::test_curl_post_empty_raw test/integration/test_http_curl.py::CurlTestCase::test_curl_post_no_expiry test/integration/test_http_curl.py::CurlTestCase::test_curl_post_no_lexer test/integration/test_http_curl.py::CurlTestCase::test_curl_post_no_raw test/integration/test_http_curl.py::CurlTestCase::test_curl_post_nonexistent_expiry test/integration/test_http_curl.py::CurlTestCase::test_curl_post_nonexistent_lexer test/integration/test_http_curl.py::CurlTestCase::test_curl_post_spaced_raw test/integration/test_http_curl.py::CurlTestCase::test_curl_raw test/integration/test_http_curl.py::CurlTestCase::test_curl_raw_spaced test/integration/test_http_curl.py::CurlTestCase::test_curl_remove test/integration/test_http_curl.py::CurlTestCase::test_curl_show test/integration/test_http_ratelimit.py::RateLimitTestCase::test_ratelimit_verification_on_endpoints test/integration/test_http_website.py::WebsiteTestCase::test_website_about test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_empty_filenames test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_empty_lexers test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_empty_raws test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_many test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_many_too_large test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_mismatched test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_multiple test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_nonexistent_expiry test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_nonexistent_lexer test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_nothing test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_only_filenames test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_only_lexers test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_only_raws test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_only_xsrf test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_raw_only_space test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_single test/integration/test_http_website.py::WebsiteTestCase::test_website_create_post_too_large test/integration/test_http_website.py::WebsiteTestCase::test_website_expiry test/integration/test_http_website.py::WebsiteTestCase::test_website_hex_nonexistent_paste test/integration/test_http_website.py::WebsiteTestCase::test_website_index test/integration/test_http_website.py::WebsiteTestCase::test_website_index_with_lexer test/integration/test_http_website.py::WebsiteTestCase::test_website_index_with_nonexistent_lexer test/integration/test_http_website.py::WebsiteTestCase::test_website_nonexistent_page test/integration/test_http_website.py::WebsiteTestCase::test_website_raw_nonexistent_paste test/integration/test_http_website.py::WebsiteTestCase::test_website_removal test/integration/test_http_website.py::WebsiteTestCase::test_website_show_nonexistent_paste test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_download test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_download_nonexistent_paste test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_hex test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_hex_nonexistent_paste test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_index_post test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_index_post_empty_code test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_index_post_no_code test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_index_post_no_expiry test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_index_post_no_lexer test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_index_post_nonexistent_expiry test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_index_post_nonexistent_lexer test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_logo test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_raw test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_remove test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_remove_nonexistent_paste test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_repaste test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_repaste_nonexistent_paste test/integration/test_http_website.py::DeprecatedWebsiteTestCase::test_website_show