auhlig: Note: 
- The rate limit algorithm was re-written in LUA (bonus points for performance) and prerequisite for traffic shaping. 

- Completely drop memcache support. Only require `Redis >= 5.0.0`.
diff --git a/ChangeLog b/ChangeLog
index f8b2503..b6dfd4c 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,16 @@
 CHANGES
 =======
 
+* closed #8
+* nothing to see
+* fix test for membership
+* fix test
+* fix the fix
+* fix import
+* rm unused import
+* fix test
+* fix pep8
+* docs
 * fix blacklisting and suspending requests; extend wsgi configuration for max\_sleep\_time\_seconds, log\_sleep\_time\_seconds
 * fix retry-after header value
 * align metric labels with watcher middleware; verify backend on start; update examples in docs
diff --git a/README.md b/README.md
index 588ff4d..265fb40 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@ OpenStack Rate Limit Middleware
 
 [![Build Status](https://travis-ci.org/sapcc/openstack-rate-limit-middleware.svg?branch=master)](https://travis-ci.org/sapcc/openstack-rate-limit-middleware)
 
-The OpenStack Rate Limit Middleware enables traffic control for OpenStack APIs per tuple of
+The OpenStack Rate Limit Middleware enforces rate limits and enables traffic shaping for OpenStack APIs per tuple of
 - *target type URI*
 - *action*
 - *scope* (project, host address)
@@ -11,15 +11,14 @@ The OpenStack Rate Limit Middleware enables traffic control for OpenStack APIs p
 It also supports enforcing global and scoped rate limits.
 More details can be found in the documentation.
 
-## Prerequisite
+## Prerequisites
 
 This middleware requires the classification for OpenStack requests.  
 The [openstack-watcher-middleware](https://github.com/sapcc/openstack-watcher-middleware) can be used to classify requests
 based on the [DMTF CADF specification](https://www.dmtf.org/standards/cadf).
 In terms of rate limiting, a request to an OpenStack service can be described by an *action*, *target type URI* and its *scope*.
 
-Moreover, this middleware uses a backend to store rate limits.  
-Thus either `Redis >= 5.0.0` (stable; preferred) or `Memcached >= 1.5.12` (beta) is required as a datastore.
+Moreover, this middleware uses `Redis >= 5.0.0` as a backend to store rate limits.
 
 ## Documentation
 
diff --git a/docs/configure.md b/docs/configure.md
index 62b2044..8884a39 100644
--- a/docs/configure.md
+++ b/docs/configure.md
@@ -3,7 +3,7 @@ Middleware configuration
 
 This sections provides an overview of the configurable options via WSGI config and configuration file.
 
-## Global and local rate limits
+# Global and local rate limits
 
 Rate limits can be enforced on 2 levels: Global and local.
   
@@ -17,7 +17,7 @@ Rate limits can be enforced on 2 levels: Global and local.
   If the number of requests per scope, action, target type URI within the specified window would exceed the configured maximum,
     a configurable rate limit response is sent until the number of requests within the window is again below the maximum.
 
-## Configure rate limits
+# Configure rate limits
 
 Rate limits can be configured via a *configuration file* and/or via [*Limes*](https://github.com/sapcc/limes). 
 The configuration file can only be used to specify global rate limits and defaults for local rate limits.
@@ -29,67 +29,14 @@ The syntax for minimal configuration of rate limits is described below.
 rates:
     <level>:
         <target_type_uri>:
+              # The name of the action.
             - action:   <action type>
-              # limit to n requests per m <unit> 
-              # valid interval units are `s, m, h, d`.
+            
+              # Limit to n requests per m <unit>. 
+              # Valid interval units are `s, m, h, d`.
               limit:    <n>r/<m><t>
 ```
 
-
-## Black- & Whitelist
-
-This middleware allows configuring a black- and whitelist for certain scopes.
-A scope might be an (initiator/target) project UUID or an initiator host address.   
-If a scope is blacklisted, the middleware immediately returns the configured blacklist response. 
-Requests in a whitelisted scope are not rate limited.  
-Also see the [examples](../etc/).  
-
-```yaml
-# List of blacklisted scopes (project UUID, host address).
-blacklist:
-    - <scope>
-
-# List of whitelisted scoped (project UUID, host address).
-whitelist:
-    - <scope>
-```
-
-## Customize responses
-
-The blacklist and rate limit responses can be configured as shown below.  
-A custom response requires the **status**, **status_code** and **body** or **json_body** to be specified.
-```yaml
-rate_limit_response:
-  # HTTP response status string.
-  status: 498 Rate Limited
-  
-  # HTTP response status code.
-  status_code: 498
-  
-  # Specify *either* body or json_body.
-  body:  "<html><body><h1>Rate limit exceeded</h1></body></html>"
-  # json_body: { "message": "rate limit exceeded" }
-  
-  # Optional: Set additional headers.
-  headers:
-    X-SERVICE: SOMETHING
-
-blacklist_response:
-  # HTTP response status string.
-  status: 497 Blacklisted
-  
-  # HTTP response status code.
-  status_code: 497
-  
-  # Specify *either* body or json_body.
-  body:  "<html><body><h1>You have been blacklisted. Contact an administrator.</h1></body></html>"
-  # json_body: { "message": "You have been blacklisted. Please contact and administrator." }
-  
-  # Optional: Set additional headers.
-  headers:
-    X-SERVICE: SOMETHING
-```
-
 ## Rate limit groups
 
 A set of CADF actions can be logically grouped and - in terms of rate limiting - be count
@@ -157,3 +104,115 @@ rates:
       - action: create 
         limit: 5r/10m
 ``` 
+
+## Black- & Whitelist
+
+This middleware allows configuring a black- and whitelist for certain scopes.
+A scope might be an (initiator/target) project UUID or an initiator host address.   
+If a scope is blacklisted, the middleware immediately returns the configured blacklist response. 
+Requests in a whitelisted scope are not rate limited.  
+Also see the [examples](../etc/).  
+
+```yaml
+# List of blacklisted scopes (project UUID, host address).
+blacklist:
+    - <scope>
+
+# List of whitelisted scoped (project UUID, host address).
+whitelist:
+    - <scope>
+```
+
+## Customize responses
+
+The blacklist and rate limit responses can be configured as shown below.  
+A custom response requires the **status**, **status_code** and **body** or **json_body** to be specified.
+```yaml
+rate_limit_response:
+  # HTTP response status string.
+  status: 498 Rate Limited
+  
+  # HTTP response status code.
+  status_code: 498
+  
+  # Specify *either* body or json_body.
+  body:  "<html><body><h1>Rate limit exceeded</h1></body></html>"
+  # json_body: { "message": "rate limit exceeded" }
+  
+  # Optional: Set additional headers.
+  headers:
+    X-SERVICE: SOMETHING
+
+blacklist_response:
+  # HTTP response status string.
+  status: 497 Blacklisted
+  
+  # HTTP response status code.
+  status_code: 497
+  
+  # Specify *either* body or json_body.
+  body:  "<html><body><h1>You have been blacklisted. Contact an administrator.</h1></body></html>"
+  # json_body: { "message": "You have been blacklisted. Please contact and administrator." }
+  
+  # Optional: Set additional headers.
+  headers:
+    X-SERVICE: SOMETHING
+```
+
+# Testing
+
+This middleware offers a variety of options for rate limiting and traffic shaping for an OpenStack API.  
+Whether the current configuration and behaviour matches the users expectations can be verified using [siege](https://github.com/JoeDog/siege) - a load testing and benchmarking toolkit.
+Install on OSX using [homebrew](https://formulae.brew.sh/formula/siege) `brew install siege`.
+
+Assuming that a valid token was issued by OpenStack Keystone and is available as `OS_AUTH_TOKEN`:
+```bash
+# Obtain token.
+export OS_AUTH_TOKEN=$(openstack token issue -c id -f value)
+
+# Send 10 concurrent requests to an endpoint and benchmark.
+siege --concurrent=1 --reps=10 --benchmark -header="X-AUTH-TOKEN:$OS_AUTH_TOKEN" https://$OpenStackURI
+```
+
+Example: 
+
+Test OpenStack Swift `POST account/container`
+
+(1) Send 3 concurrent POST requests to update a Swift container using `siege`:
+```bash
+siege --concurrent=1 --reps=3 --benchmark --header="X-AUTH-TOKEN:$OS_AUTH_TOKEN" 'https://$OpenStackURI/v1/AUTH_$ProjectID/mycontainer POST'
+```
+
+The output for a rate of `1r/m` and `max_delay_seconds=0` (nodelay) could look as follows:
+```bash
+The server is now under siege...
+HTTP/1.1 204     0.20 secs:       0 bytes ==> POST https://$OpenStackURI/v1/AUTH_$ProjectID/mycontainer
+HTTP/1.1 498     0.09 secs:      19 bytes ==> POST https://$OpenStackURI/v1/AUTH_$ProjectID/mycontainer
+HTTP/1.1 498     0.11 secs:      19 bytes ==> POST https://$OpenStackURI/v1/AUTH_$ProjectID/mycontainer
+```
+Only the 1st request is processed and the 2 subsequent, almost concurrently issued, requests are rejected as the rate limit of `1r/m` is exceeded.
+
+(2) Send 10 requests with a a random delay of 1s to 10s between each request:
+```bash
+siege --reps=10 --delay=10 --header="X-AUTH-TOKEN:$OS_AUTH_TOKEN" 'https://$OpenStackURI/v1/AUTH_$ProjectID/mycontainer POST'
+```
+
+The output for a rate of `2r/m` and `max_delay_seconds=20` could look as follows:
+```bash
+The server is now under siege...
+HTTP/1.1 204     0.36 secs:       0 bytes ==> POST https://$OpenStackURI/v1/AUTH_$ProjectID/mycontainer
+HTTP/1.1 204     0.22 secs:       0 bytes ==> POST https://$OpenStackURI/v1/AUTH_$ProjectID/mycontainer
+HTTP/1.1 498     0.10 secs:      19 bytes ==> POST https://$OpenStackURI/v1/AUTH_$ProjectID/mycontainer
+HTTP/1.1 498     0.11 secs:      19 bytes ==> POST https://$OpenStackURI/v1/AUTH_$ProjectID/mycontainer
+HTTP/1.1 498     0.12 secs:      19 bytes ==> POST https://$OpenStackURI/v1/AUTH_$ProjectID/mycontainer
+HTTP/1.1 498     0.10 secs:      19 bytes ==> POST https://$OpenStackURI/v1/AUTH_$ProjectID/mycontainer
+HTTP/1.1 204    19.72 secs:       0 bytes ==> POST https://$OpenStackURI/v1/AUTH_$ProjectID/mycontainer
+HTTP/1.1 204     0.11 secs:       0 bytes ==> POST https://$OpenStackURI/v1/AUTH_$ProjectID/mycontainer
+...
+```
+
+The 1st and 2nd request are successfully processed within the rate limit of `2r/m`. 
+The 3rd to 6th requests are rejected as the rate limit is exceeded and the request would need to be suspended for longer than `max_delay_seconds`.
+However, the 7th request still exceeds the rate limit but reaches the API just less than 20s before it could be successfully processed according to the rate limit.
+Thus it is suspended for ~19 seconds and processed afterwards as indicated by the relatively long transaction time.
+Request #8 is successfully processed, but  
diff --git a/docs/draft_limes.md b/docs/draft_limes.md
index 3d1dafc..d60e251 100644
--- a/docs/draft_limes.md
+++ b/docs/draft_limes.md
@@ -1,11 +1,15 @@
 Limes integration
 =================
 
-[Limes](https://github.com/sapcc/limes) currently reports resource quota and usage for a project as described [here](https://github.com/sapcc/limes/blob/master/docs/users/api-v1-specification.md#get-v1domainsdomain_idprojectsproject_id).
-Add rate limits by extending the JSON document as follows.
+Global rate limits and defaults for each project can be defined via a [configuration file](./configure.md).  
+However, the configuration file does not allow defining rate limits specific for a project.  
+This can only be done using [Limes](https://github.com/sapcc/limes) as outlined below. 
+Limes reports rate limits for a project as described [here](https://github.com/sapcc/limes/blob/master/docs/users/api-v1-specification.md#get-v1domainsdomain_idprojectsproject_id).
+
+Note the usage of the `rate=only` query parameter to only report rate limits.
 
 Example:  
-**GET /v1/domains/:domain_id/projects/:project_id**
+**GET /v1/domains/:domain_id/projects/:project_id?rates=only**
 ```
 {
     "projects": [
@@ -15,12 +19,7 @@ Example:
             "services": [
                 {
                     "type": "object-store",
-                    "area": "storage",
-                    "resources": [
-                        {
-                        ...
-                        }
-                    ],
+                    "area": "storage"
                     "rates": [
                         {
                             "targetTypeURI": "account/container",
@@ -43,7 +42,7 @@ Example:
 }
 ```
 
-**Defaults** for each project will be deployed using limes' constraints.
+**Defaults** for each project may be deployed using limes' constraints.
 
 ```
 limes: 
diff --git a/docs/install.md b/docs/install.md
index f80267c..75da403 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -19,54 +19,63 @@ pipeline = .. sapcc-watcher sapcc-rate-limit ..
 The following parameters are provided via the WSGI configuration:
 ```yaml
 # The service type according to CADF specification.
-service_type:     <string>
+service_type:                   <string>
 
 # Path to the configuration file.
-config_file:      <string>
+config_file:                    <string>
 
 # If this middleware enforces rate limits in multiple replicas of an API,
 # the clock accuracy of the individual replicas can be configured as follows.
-clock_accuracy:   <n><unit> (default 1ms)
+clock_accuracy:                 <n><unit> (default: 1ms)
 
 # Per default rate limits are applied based on `initiator_project_id`.
 # However, this can also be se to `initiator_host_address` or `target_project_id`.
-rate_limit_by:    <string>
+rate_limit_by:                  <string>
 
 # The maximal time a request can be suspended in seconds.
 # Instead of immediately returning a rate limit response, a request can be suspended
 # until the specified maximum duration to fit the configured rate limit. 
-max_sleep_time_seconds: <int> (default: 20)
+# This feature can be disabled by setting the max sleep time to 0 seconds.
+max_sleep_time_seconds:         <int> (default: 20)
 
 # Log requests that are going to be suspended for log_sleep_time_seconds <= t <= max_sleep_time_seconds.
-log_sleep_time_seconds: <int> (default: 10)
+log_sleep_time_seconds:         <int> (default: 10)
 
 # Emit Prometheus metrics via StatsD.
 # Host of the StatsD exporter.
-statsd_host:      <string> (default: 127.0.0.1)
+statsd_host:                    <string> (default: 127.0.0.1)
 
 # Port of the StatsD exporter.
-statsd_port:      <int> (default: 9125)
+statsd_port:                    <int> (default: 9125)
 
 # Prefix to apply to all metrics provided by this middleware.
-statsd_prefix:    <string> (default: openstack_ratelimit_middleware)
+statsd_prefix:                  <string> (default: openstack_ratelimit_middleware)
 
-# The backend used to store number of requests.
-# Choose between redis, memcache.
-backend:          <string> (default: redis)
+# Host for redis backend.
+backend_host:                   <string> (default: 127.0.0.1)
 
-# Host for backend.
-backend_host:     <string> (default: 127.0.0.1)
+# Port for redis backend.
+backend_port:                   <int> (default: 6379)
 
-# Port for backend.
-backend_port:     <int> (default: 6379)
+## Configure Limes as provider for rate limits.
+# See the limes guide for more details.
+limes_enabled:                  <bool> (default: false)
+
+# URI of the Limes API.
+# If not provided, the middleware attempts to autodiscover the URI of the Limes API using the  
+# service catalog of the Keystone token.
+limes_api_uri:                  <string>
+
+# To avoid querying for rate limits for each requests, rate limits obtained from Limes are cached in Redis.
+# Specify the interval in which cached rate limits are refreshed in seconds.
+# Setting 0 here disabled the caching. The middleware will query Limes for rate limits for every requests.
+# This might have a negative effect on your applications performance.
+limes_refresh_interval_seconds: <int> (default: 300)
 
-## Limes configuration.
-# Rate limits con be provided via Limes.
-limes_enabled:    <bool> (default: false)
 # Credentials of the OpenStack service user able to read rate limits from Limes.
-auth_url:         <string>
-username:         <string>
-user_domain_name: <string>
-password:         <string>
-domain_name:      <string>
+identity_auth_url:              <string>
+username:                       <string>
+user_domain_name:               <string>
+password:                       <string>
+domain_name:                    <string>
 ```
diff --git a/docs/user.md b/docs/user.md
index d4f2b15..235590b 100644
--- a/docs/user.md
+++ b/docs/user.md
@@ -33,6 +33,7 @@ X-RateLimit-Reset: 60
 X-Retry-After: 60
 ```
 
+
 # Metrics
 
 This middleware emits the following [Prometheus metrics](https://prometheus.io/docs/concepts/metric_types) via [StatsD](https://github.com/DataDog/datadogpy).  
@@ -53,3 +54,26 @@ All metrics come with the following labels:
 | action          | The CADF action of the request. |
 | scope           | The scope of the request. |
 | target_type_uri | The CADF target type URI of the request. |
+
+
+# Burst requests
+
+This middleware is capable of handling a burst of requests as described hereinafter.
+
+## With delay
+
+This middleware handles requests that would exceed the configured rate by delaying them until the next possible slot but not longer than `max_sleep_time_seconds`.
+See the [WSGI section](install.md) on how to configure this. 
+
+Example:  
+Given a `rate limit=1r/m` and a `max_sleep_time_seconds=20`, the 1st request at t<sub>1</sub>=0 would be processed just fine. 
+However, a 2nd request received within the one minute window after the 1st request would exceed the rate limit.
+Assuming it's received at t<sub>2</sub>=45, the request would not be rejected but suspended for 15 seconds and processed afterwards so that the rate of `1r/m` is not exceeded. 
+
+However, this behaviour might let your application appear slow for a user. 
+It can be disabled by setting `max_sleep_time_seconds=0` and using burst requests without delay as described below.
+
+## Without delay
+
+Handling burst requests without delay 
+
diff --git a/rate_limit/backend.py b/rate_limit/backend.py
index 34eb009..0c84af0 100644
--- a/rate_limit/backend.py
+++ b/rate_limit/backend.py
@@ -13,9 +13,8 @@
 # under the License.
 
 import eventlet
+import hashlib
 import logging
-import math
-import memcache
 import redis
 import time
 
@@ -49,9 +48,9 @@ class Backend(object):
         """
         return None
 
-    def is_backend_available(self):
+    def is_available(self):
         """
-        Check whether the backend is available and supported.
+        Check whether the backend is available and the version supported.
 
         :return: bool whether it's available, string describing the error (if any)
         """
@@ -76,7 +75,26 @@ class RedisBackend(Backend):
         self.__max_sleep_time_seconds = max_sleep_time_seconds
         self.__log_sleep_time_seconds = log_sleep_time_seconds
         self.__rate_limit_response = rate_limit_response
-        self.__redis_conn_pool = redis.ConnectionPool(host=host, port=port)
+        self.__timeout = kwargs.get('timeout', 20)
+        self.__max_connections = kwargs.get('max_connections', 100)
+        # Use a thread-safe blocking connection pool.
+        conn_pool = redis.BlockingConnectionPool(
+            host=host, port=port, max_connections=self.__max_connections, timeout=self.__timeout,
+        )
+        self.__redis = redis.StrictRedis(
+            connection_pool=conn_pool, decode_responses=True,
+            socket_timeout=self.__timeout, socket_connect_timeout=self.__timeout,
+        )
+
+        script_name = "redis_sliding_window.lua"
+        script = common.load_lua_script(script_name)
+        if not script:
+            self.logger.error(
+                "error loading rate limit script: '{0}'".format(script_name)
+            )
+            return
+        self.__rate_limit_script = script
+        self.__rate_limit_script_sha = hashlib.sha1(script.encode('utf-8')).hexdigest()
 
     def is_available(self):
         """Check whether the redis is available and supported."""
@@ -93,10 +111,9 @@ class RedisBackend(Backend):
         :return: bool
         """
         try:
-            redis_client = redis.StrictRedis(connection_pool=self.__redis_conn_pool, decode_responses=True)
             # Invoke get to test redis connection.
             # Will return None or one of the following exceptions.
-            redis_client.get("")
+            self.__redis.get("")
         except (redis.exceptions.ConnectionError, redis.exceptions.BusyLoadingError):
             return False
         return True
@@ -108,8 +125,7 @@ class RedisBackend(Backend):
 
         :return: bool
         """
-        redis_client = redis.StrictRedis(connection_pool=self.__redis_conn_pool, decode_responses=True)
-        version = redis_client.info().get('redis_version', None)
+        version = self.__redis.info().get('redis_version', None)
         if not version:
             return False
         return bool(StrictVersion(version) >= StrictVersion('3.0.0'))
@@ -140,42 +156,34 @@ class RedisBackend(Backend):
         # Convert from float to int with millisecond accuracy.
         now_int = int(now * 1000)
         window_seconds_int = int(window_seconds * 1000)
-
         # Max. lookback as timestamp.
-        lookback_time_max = now_int - window_seconds_int
+        lookback_time_max = int(now_int - window_seconds_int)
+        # Make sure it's an int.
+        max_calls_int = int(max_calls)
 
-        # Increase performance by using a pipeline to buffer multiple commands to the redis backend in a single request.
-        pipe = redis.StrictRedis(connection_pool=self.__redis_conn_pool, decode_responses=True).pipeline()
-        # Remove all API calls that are older than the sliding window.
-        pipe.zremrangebyscore(key, '-inf', lookback_time_max)
-        # List of API calls during sliding window.
-        pipe.zrange(key, 0, -1)
-        # Add current API call with timestamp.
-        pipe.zadd(key, {now_int: now_int})
-        # Reset expiry time for key.
-        pipe.expire(key, window_seconds_int)
-        # Execute the transaction block.
-        result = pipe.execute()
+        # Use the SHA1 digest of the LUA script and use redis internal caching instead of sending it every time.
+        try:
+            result = self.__redis.evalsha(
+                self.__rate_limit_script_sha, 6, key, lookback_time_max, now_int,
+                max_calls_int, window_seconds_int, self.__max_sleep_time_seconds
+            )
+        # If the script is not (yet) present send and evaluate it.
+        except redis.exceptions.NoScriptError:
+            result = self.__redis.eval(
+                self.__rate_limit_script, 6, key, lookback_time_max, now_int,
+                max_calls_int, window_seconds_int, self.__max_sleep_time_seconds
+            )
 
-        timestamps = []
-        if result and len(result) >= 1:
-            timestamps = result[1]
+        # Parse result list safely.
+        remaining = common.listitem_to_int(result, idx=0)
+        retry_after_seconds = common.listitem_to_int(result, idx=1)
 
-        # Check whether rate limit is exhausted.
-        remaining = int(max_calls - len(timestamps))
         # Return here if we still have remaining requests.
         if remaining > 0:
             return None
 
-        # Check if the request should be suspended.
-        # Get seconds until another request would be possible according to rate limit. Always round up.
-        timestamp0 = now_int
-        if len(timestamps) > 0:
-            timestamp0 = int(timestamps[0])
-
-        retry_after_seconds = int(math.ceil(timestamp0 + window_seconds_int - now_int) / 1000)
         # Suspend the current request if its it has to wait no longer than max_sleep_time_seconds.
-        if retry_after_seconds <= self.__max_sleep_time_seconds:
+        elif retry_after_seconds <= self.__max_sleep_time_seconds:
             # Log the current request if it has to be suspended for at least log_sleep_time_seconds.
             if retry_after_seconds >= self.__log_sleep_time_seconds:
                 self.logger.info(
@@ -193,107 +201,3 @@ class RedisBackend(Backend):
             retry_after=retry_after_seconds
         )
         return self.__rate_limit_response
-
-
-class MemcachedBackend(Backend):
-    """Beta Memcached backend for storing rate limits."""
-
-    def __init__(self, host, port, rate_limit_response, max_sleep_time_seconds, log_sleep_time_seconds, logger, **kwargs):
-        super(MemcachedBackend, self).__init__(
-            host=host,
-            port=port,
-            rate_limit_response=rate_limit_response,
-            max_sleep_time_seconds=max_sleep_time_seconds,
-            log_sleep_time_seconds=log_sleep_time_seconds,
-            logger=logger,
-            kwargs=kwargs
-        )
-        self.__host = host
-        self.__port = port
-        self.__rate_limit_response = rate_limit_response
-        self.__max_sleep_time_seconds = max_sleep_time_seconds
-        self.__log_sleep_time_seconds = log_sleep_time_seconds
-        self.__memcached = memcache.Client(
-            servers=[host],
-            debug=1
-        )
-
-    def is_backend_available(self):
-        if not self.__memcached.set("test", 1):
-            return False, "rate limit failed. memcached not available. host='{0}', port='{1}'".format(self.__host, str(self.__port))
-        return True, ""
-
-    def rate_limit(self, scope, action, target_type_uri, max_rate_string):
-        """
-        Handle the rate limit for the given scope, action, target_type_uri and max_rate_string.
-        If scope is not given (scope=None) the global (non-project specific) rate limit is checked.
-
-        :param scope: the scope (project uuid, host ip, etc., ..) or None for global rate limits
-        :param action: the CADF action
-        :param target_type_uri: the CADF target type URI
-        :param max_rate_string: the max. rate limit per sliding window
-        :return: the configured RateLimitResponse or None
-        """
-        try:
-            max_rate, sliding_window_seconds = Units.parse_sliding_window_rate_limit(max_rate_string)
-            key = common.key_func(scope, action, target_type_uri)
-            self.logger.debug(
-                "checking rate limit for request '{0} {1}' in scope {2}".format(action, target_type_uri, scope)
-            )
-            return self.__rate_limit(key, sliding_window_seconds, max_rate, max_rate_string)
-        except Exception as e:
-            self.logger.error('MemcacheConnectionError: {0}'.format(e))
-            return 0
-
-    def __rate_limit(self, key, window_seconds, max_calls, max_rate_string):
-        now = time.time()
-
-        # get list of requests that hit the api
-        hit_list = self.__memcached.gets(key)
-        if not hit_list or not isinstance(hit_list, list):
-            hit_list = []
-        hit_list = self.__get_hits_in_current_window(hit_list, now - window_seconds)
-
-        num_req = len(hit_list) + 1
-        rate_limit_reached = num_req > max_calls
-        if rate_limit_reached:
-            self.logger.debug(
-                'rate limit reached key: {0}, max_rate: {1}, window_seconds: {2}'
-                .format(key, max_calls, window_seconds)
-            )
-            self.__rate_limit_response.set_headers(
-                max_rate_string,
-                max_calls - num_req,
-                math.ceil(hit_list[0] + window_seconds - now)
-            )
-            return self.__rate_limit_response
-        # update number of requests and make sure the key expires eventually to avoid polluting the memcache
-        self.__memcached.cas(key, hit_list + [now], time=2 * window_seconds)
-        return None
-
-    def key_with_time_window(self, scope, action, target_type_uri, time_window):
-        """
-        Return a key with a time window suffix, e.g. '012314af_create_servers_11-04-2018_13:00:29'.
-
-        :param scope: the scope uid of the request
-        :param action: cadf action the request
-        :param target_type_uri: the target type uri
-        :param time_window: the sliding time window
-        :return: the key with time slot prefix
-        """
-        return '{0}_{1}'.format(common.key_func(scope, action, target_type_uri), time_window)
-
-    @staticmethod
-    def __get_hits_in_current_window(timestamp_list, not_older_than_timestamp=time.time()):
-        """
-        Get list of requests (actually their timestamps) that hit the api within the current sliding window.
-        If any left: also expire outdated timestamps.
-
-        :param timestamp_list: list of timestamps
-        :param not_older_than_timestamp: delete items older than this timestamp
-        :return: the cleaned timestamp list
-        """
-        for ts in timestamp_list:
-            if ts < not_older_than_timestamp:
-                timestamp_list.remove(ts)
-        return timestamp_list
diff --git a/rate_limit/common.py b/rate_limit/common.py
index e07dc93..b791d37 100644
--- a/rate_limit/common.py
+++ b/rate_limit/common.py
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import os
 import time
 import yaml
 
@@ -38,16 +39,19 @@ class Constants(object):
     log_sleep_time_seconds = 'log_sleep_time_seoncds'
     unknown = 'unknown'
 
-    # rate limit by ..
+    # Rate limit by ..
     initiator_project_id = 'initiator_project_id'
     initiator_host_address = 'initiator_host_address'
     target_project_id = 'target_project_id'
 
-    # fetch rate limits from limes every t seconds
-    limes_refresh_interval_seconds = 300
+    # Interval in which cached rate limits are refreshed in seconds.
+    limes_refresh_interval_seconds = 'limes_refresh_interval_seconds'
 
-    backend_redis = 'redis'
-    backend_memcache = 'memcache'
+    # The URI of the Limes API.
+    limes_api_uri = 'limes_api_uri'
+
+    # Type of the Limes service as found in token service catalog.
+    limes_service_type = 'limes'
 
     # The limit for the current request in the format <n>r/<m><t>.
     # Read: Limit to n requests per m <unit>. Valid interval units are `s, m, h, d`.
@@ -148,3 +152,69 @@ def load_config(cfg_file):
         raise errors.ConfigError("Failed to load configuration from file %s: %s" % (cfg_file, str(e)))
     finally:
         return yaml_conf
+
+
+def to_int(raw_value, default=0):
+    """
+    Safely parse a raw value and convert to an integer.
+    If that fails return the default value.
+
+    :param raw_value: the raw value
+    :param default: the fallback value if conversion fails
+    :return: the value as int or None
+    """
+    try:
+        return int(float(raw_value))
+    except (ValueError, TypeError):
+        return default
+
+
+def listitem_to_int(listthing, idx, default=0):
+    """
+    Safely get an item by index from a list.
+
+    :param listthing: the list
+    :param idx: the index of the item
+    :param default: the default value if item not found
+    :return: the item as int or the default value
+    """
+    try:
+        return to_int(listthing[idx], default)
+    except (IndexError, TypeError):
+        return default
+
+
+def load_lua_script(filename, foldername="lua"):
+    """
+    Load a lua scripts.
+
+    :param filename: the filename of the script
+    :param foldername: the name of the folder containing the script
+    :return: the content or None
+    """
+    content = None
+    path = os.path.join(
+        os.path.dirname(os.path.abspath(__file__)),
+        "{0}/{1}".format(foldername, filename)
+    )
+    try:
+        f = open(path)
+        content = f.read()
+        f.close()
+    except IOError:
+        return None
+    finally:
+        return content
+
+
+def build_uri(base, path):
+    """
+    Build the URI using base and path.
+
+    :param base:
+    :param path:
+    :return:
+    """
+    base = base.rstrip('/')
+    path = path.lstrip('/')
+    return base + '/' + path
diff --git a/rate_limit/errors.py b/rate_limit/errors.py
index f0749c8..0807eda 100644
--- a/rate_limit/errors.py
+++ b/rate_limit/errors.py
@@ -19,18 +19,6 @@ class ConfigError(Exception):
     pass
 
 
-class MemcacheConnectionError(Exception):
-    """Raised when connection to memcached caused an error."""
-
-    pass
-
-
-class MaxSleepTimeHitError(Exception):
-    """Raised when the maximal sleep time was hit for a key."""
-
-    pass
-
-
 class UnitConversionError(Exception):
     """Raised when a unit is invalid and cannot be converted."""
 
diff --git a/rate_limit/lua/redis_sliding_window.lua b/rate_limit/lua/redis_sliding_window.lua
new file mode 100644
index 0000000..affc77f
--- /dev/null
+++ b/rate_limit/lua/redis_sliding_window.lua
@@ -0,0 +1,46 @@
+local key, lookback_timestamp_max_int, now_int, max_calls_int, window_seconds_int, max_sleep_time_seconds_int
+key = tostring(KEYS[1])
+lookback_timestamp_max_int = tonumber(KEYS[2])
+now_int = tonumber(KEYS[3])
+max_calls_int = tonumber(KEYS[4])
+window_seconds_int = tonumber(KEYS[5])
+max_sleep_time_seconds_int = tonumber(KEYS[6])
+-- Rate limit algorithm inspired by https://engineering.classdojo.com/blog/2015/02/06/rolling-rate-limiter
+-- While this works well for rate limiting we need a slightly more advanced lua script for shaping the traffic,
+-- like allowing burst requests with delayed/not delayed execution.
+-- Remove all API calls that are older than the sliding window.
+redis.call('zremrangebyscore', key, '-inf', lookback_timestamp_max_int)
+-- List of API calls in the current sliding window.
+local reqs, remaining, timestamp0, retry_after_seconds
+reqs = redis.call('zrange', key, 0, -1)
+-- Get number of remaining requests.
+remaining = tonumber(max_calls_int - #reqs)
+
+-- Add timestamp of current request if there are remaining requests (aka rate limit not reached).
+if remaining > 0 then
+    -- Add timestamp if we still have remaining requests.
+    redis.call('zadd', key, now_int, now_int)
+    -- Reset expiry time for key.
+    redis.call('expire', key, window_seconds_int)
+    -- Return the number of remaining requests. Retry after not relevant.
+    return {remaining, 0}
+end
+
+-- Rate limit reached but check if the requests can be suspended.
+-- Get timestamp of 1st requests in current window.
+timestamp0 = reqs[1] or now_int
+-- Calculate how long the request would need to be suspended.
+retry_after_seconds = tonumber(math.ceil(timestamp0 + window_seconds_int - now_int) / 1000)
+-- Can be suspended?
+if retry_after_seconds <= max_sleep_time_seconds_int then
+    -- Count current request.
+    remaining = remaining -1
+    -- Time when requests will be executed.
+    now_int = tonumber(now_int + retry_after_seconds * 1000)
+    -- Add timestamp to the list.
+    redis.call('zadd', key, now_int, now_int)
+    -- Reset expiry time for key.
+    redis.call('expire', key, window_seconds_int)
+end
+-- Returns the number of remaining requests and the list of timestamps.
+return {remaining, retry_after_seconds}
diff --git a/rate_limit/provider.py b/rate_limit/provider.py
index 5d8b510..3acc8db 100644
--- a/rate_limit/provider.py
+++ b/rate_limit/provider.py
@@ -13,13 +13,13 @@
 # under the License.
 
 
+import keystoneclient.v3 as keystonev3
 import logging
-import memcache
+import redis
 import requests
 
 from keystoneauth1.identity import v3
 from keystoneauth1 import session
-from keystoneclient.v3 import client
 
 from . import common
 
@@ -29,7 +29,7 @@ logging.basicConfig(level=logging.ERROR, format='%(asctime)-15s %(message)s')
 class RateLimitProvider(object):
     """Interface to obtain rate limits from different sources."""
 
-    def __init__(self, service_type, refresh_interval_seconds, logger=logging.getLogger(__name__), **kwargs):
+    def __init__(self, service_type, logger=logging.getLogger(__name__), **kwargs):
         self.service_type = service_type
         self.logger = logger
         self.global_ratelimits = {}
@@ -40,7 +40,7 @@ class RateLimitProvider(object):
         Get the global rate limit per action and target type URI.
         Returns -1 if unlimited.
 
-        :param action: the cadf action for the request
+        :param action: the CADF action for the request
         :param target_type_uri: the target type URI of the request
         :param kwargs: optional, additional parameters
         :return: the global rate limit or -1 if not set
@@ -52,7 +52,7 @@ class RateLimitProvider(object):
         Get the local (project/domain/ip, ..) rate limit per scope, action, target type URI.
 
         :param scope: the UUID of the project, domain or the IP
-        :param action: the cadf action of the request
+        :param action: the CADF action of the request
         :param target_type_uri: the target type URI of the request
         :param kwargs: optional, additional parameters
         :return: the local rate limit or -1 if not set
@@ -63,9 +63,9 @@ class RateLimitProvider(object):
 class ConfigurationRateLimitProvider(RateLimitProvider):
     """The provider to obtain rate limits from a configuration file."""
 
-    def __init__(self, service_type, refresh_interval_seconds, logger=logging.getLogger(__name__), **kwargs):
+    def __init__(self, service_type, logger=logging.getLogger(__name__), **kwargs):
         super(ConfigurationRateLimitProvider, self).__init__(
-            service_type=service_type, refresh_interval_seconds=refresh_interval_seconds, logger=logger, kwargs=kwargs
+            service_type=service_type, logger=logger, kwargs=kwargs
         )
 
     def get_global_rate_limits(self, action, target_type_uri, **kwargs):
@@ -73,7 +73,7 @@ class ConfigurationRateLimitProvider(RateLimitProvider):
         Get the global rate limit per action and target type URI.
         Returns -1 if unlimited.
 
-        :param action: the cadf action for the request
+        :param action: the CADF action for the request
         :param target_type_uri: the target type URI of the request
         :param kwargs: optional, additional parameters
         :return: the global rate limit or -1 (unlimited) if not set
@@ -90,7 +90,7 @@ class ConfigurationRateLimitProvider(RateLimitProvider):
         Get the local (project/domain/ip, ..) rate limit per scope, action, target type URI.
 
         :param scope: the UUID of the project, domain or the IP
-        :param action: the cadf action of the request
+        :param action: the CADF action of the request
         :param target_type_uri: the target type URI of the request
         :param kwargs: optional, additional parameters
         :return: the local rate limit or -1 if not set
@@ -117,23 +117,42 @@ class ConfigurationRateLimitProvider(RateLimitProvider):
 class LimesRateLimitProvider(RateLimitProvider):
     """The provider to obtain rate limits from limes."""
 
-    def __init__(self, service_type, refresh_interval_seconds, logger=logging.getLogger(__name__), **kwargs):
-        super(LimesRateLimitProvider, self).__init__(
-            service_type=service_type, refresh_interval_seconds=refresh_interval_seconds, logger=logger, kwargs=kwargs
-        )
+    def __init__(self, service_type, logger=logging.getLogger(__name__), **kwargs):
+        super(LimesRateLimitProvider, self).__init__(service_type=service_type, logger=logger, kwargs=kwargs)
 
-        # Limes provider will use memcached to store ratelimits for a configurable time (refresh_interval_seconds).
-        memcache_host = '127.0.0.1'
-        if kwargs:
-            memcache_host = kwargs.get('memcache_host', '127.0.0.1')
+        # Cache rate limits in redis if refresh_interval_seconds != 0
+        self.__refresh_interval_seconds = kwargs.get('refresh_interval_seconds', 300)
 
-        self.memcached = memcache.Client(
-            servers=[memcache_host],
-            debug=1
+        timeout = kwargs.get('redis_timeout', 20)
+        # Use a thread-safe blocking connection pool.
+        conn_pool = redis.BlockingConnectionPool(
+            host=kwargs.get('redis_host', '127.0.0.1'),
+            port=kwargs.get('redis_port', 6379),
+            max_connections=kwargs.get('max_connections', 100),
+            timeout=kwargs.get('timeout', 20)
         )
+        self.__redis = redis.StrictRedis(
+            connection_pool=conn_pool, decode_responses=True,
+            socket_timeout=timeout, socket_connect_timeout=timeout,
+        )
+
+        # For testing purposes.
+        cli = kwargs.get('keystone_client', None)
+        if cli:
+            self.__keystone_client = cli
+        else:
+            self.__keystone_client = self.__authenticate(
+                auth_url=kwargs.get('auth_url'),
+                username=kwargs.get('username'),
+                password=kwargs.get('password'),
+                domain_name=kwargs.get('domain_name'),
+                user_domain_name=kwargs.get('user_domain_name')
+            )
 
-        self.keystone = None
-        self.limes_base_url = None
+        limes_api_uri = kwargs.get(common.Constants.limes_api_uri)
+        if not limes_api_uri:
+            limes_api_uri = self.__get_limes_base_url()
+        self.__limes_base_url = limes_api_uri
 
     def get_global_rate_limits(self, action, target_type_uri, **kwargs):
         """
@@ -145,7 +164,7 @@ class LimesRateLimitProvider(RateLimitProvider):
         :param kwargs: optional, additional parameters
         :return: the global rate limit or -1 if not set
         """
-        # TODO: global rate limits via configuration file or limes constraints?
+        # TODO: Global rate limits via Limes.
         return -1
 
     def get_local_rate_limits(self, scope, action, target_type_uri, **kwargs):
@@ -167,18 +186,19 @@ class LimesRateLimitProvider(RateLimitProvider):
         )
 
         # Find the current project by id.
-        project = self._find_project_by_id_in_list(scope, rate_limit_list.get('projects', []))
+        project = self.__find_project_by_id_in_list(scope, rate_limit_list.get('projects', []))
         # find the current service by type
-        service = self._find_service_by_type_in_list(self.service_type, project.get('services', []))
+        service = self.__find_service_by_type_in_list(self.service_type, project.get('services', []))
         # find the rates by target type URI
-        rate = self._find_rate_by_target_type_uri_in_list(target_type_uri, service.get('rates', []))
+        rate = self.__find_rate_by_target_type_uri_in_list(target_type_uri, service.get('rates', []))
         # finally find the limit
-        limit = self._find_limit_by_action_in_list(action, rate.get('actions', []))
+        limit = self.__find_limit_by_action_in_list(action, rate.get('actions', []))
         if limit:
             return limit
         return -1
 
-    def authenticate(self, auth_url, username, user_domain_name, password, domain_name):
+    def __authenticate(self, auth_url, username, user_domain_name, password, domain_name):
+        keystone_client = None
         try:
             self.logger.debug(
                 'attempting authentication using with '
@@ -195,25 +215,38 @@ class LimesRateLimitProvider(RateLimitProvider):
                 reauthenticate=True
             )
             sess = session.Session(auth=auth)
-            self.keystone = client.Client(session=sess)
+            keystone_client = keystonev3.Client(session=sess)
             self.logger.debug('successfully created keystone client and obtained token')
+
         except Exception as e:
             self.logger.error('failed to create keystone client: {0}'.format(str(e)))
 
-    def _get_limes_base_url(self, interface='public'):
+        finally:
+            return keystone_client
+
+    def __get_limes_base_url(self, interface='public'):
+        """
+        Get Limes endpoint from service catalog.
+
+        :param interface: the interface of the endpoint
+        :return: the limes endpoint
+        """
         limes_base_url = ''
         try:
             limes_service_id = None
 
-            service_list = self.keystone.services.list()
+            # Get list of services from keystone.
+            service_list = self.__keystone_client.services.list()
             if not service_list:
                 return
 
+            # Find Limes service by name in service catalog.
             for service in service_list:
-                if service.name == 'limes':
+                if service.name == common.Constants.limes_service_type:
                     limes_service_id = service.id
 
-            endpoint_list = self.keystone.endpoints.list()
+            # Get Limes endpoint.
+            endpoint_list = self.__keystone_client.endpoints.list()
             if not endpoint_list:
                 return
 
@@ -224,10 +257,12 @@ class LimesRateLimitProvider(RateLimitProvider):
                     break
 
             self.logger.warning("could not find limes base url in endpoints")
+
         except Exception as e:
             self.logger.error("error looking up limes base url: {0}".format(str(e)))
+
         finally:
-            self.limes_base_url = limes_base_url
+            return limes_base_url
 
     def list_ratelimits_for_projects_in_domain(self, project_id, domain_id=None):
         """
@@ -245,49 +280,52 @@ class LimesRateLimitProvider(RateLimitProvider):
         if not common.is_none_or_unknown(project_id):
             path += '/projects/{0}'.format(project_id)
 
+        # List only rate limits and filter for the current service.
         params = {
-            'service': self.service_type
+            'service': self.service_type,
+            'rates': 'only'
         }
-        self.logger.debug("getting rate limits from limes: {0}".format(path))
         return self._get(path, params)
 
-    def _get_rate_limit_from_limes_response(self, scope, action, target_type_uri, domain_id):
+    def __get_rate_limit_from_limes_response(self, scope, action, target_type_uri, domain_id):
         rate_limit = None
         try:
-            key = 'ratelimit_limes_{0}'.format(domain_id)
-            limes_ratelimits = self.memcached.get(key)
-            if not limes_ratelimits:
-                # Expired from memcached. Get fresh from limes and store again.
-                limes_ratelimits = self.limes.list_ratelimits_for_projects_in_domain(domain_id)
-                self.memcached.set(
-                    key=key, val=limes_ratelimits, time=self.limes_refresh_interval_seconds, noreply=True
-                )
-
-            # Parse limes response.
-            project_list = limes_ratelimits.get('projects', [])
-            service_list = self._find_service_in_project_list(project_list, scope)
-            rate_list = self._find_rate_in_service_list(service_list, self.service_type)
-            action_list = self._find_action_in_rate_list(rate_list, target_type_uri)
-            rate_limit = self._find_limit_in_action_list(action_list, action)
+            key = 'limes_{0}'.format(
+                common.key_func(scope=scope, action=action, target_type_uri=target_type_uri)
+            )
+
+            # Get rate limit from cache or get fresh from limes and store in cache.
+            rate_limit = self.__redis.get(name=key)
+            if not rate_limit:
+                # Get and parse rate limit from limes response.
+                limes_ratelimit_raw = self.list_ratelimits_for_projects_in_domain(project_id='', domain_id=domain_id)
+                project_list = limes_ratelimit_raw.get('projects', [])
+                service_list = self.__find_service_in_project_list(project_list, scope)
+                rate_list = self.__find_rate_in_service_list(service_list, self.service_type)
+                action_list = self.__find_action_in_rate_list(rate_list, target_type_uri)
+                rl = self.__find_limit_in_action_list(action_list, action)
+                # There might be no rate limit configured or we cannot get it due to an error.
+                if rl:
+                    self.__redis.setex(name=key, value=rl, time=self.__refresh_interval_seconds)
 
         except Exception as e:
             self.logger.error(
-                "could not extract limts for action '{0}', target_type_uri: '{1}', scope: '{2}' from limes: {3}"
+                "could not extract limits for action '{0}', target_type_uri: '{1}', scope: '{2}' from limes: {3}"
                 .format(action, target_type_uri, scope, str(e))
             )
         finally:
             return rate_limit
 
-    def _find_project_by_id_in_list(self, project_id, project_list):
+    def __find_project_by_id_in_list(self, project_id, project_list):
         return common.find_item_by_key_in_list(project_id, 'id', project_list)
 
-    def _find_service_by_type_in_list(self, service_type, service_list):
+    def __find_service_by_type_in_list(self, service_type, service_list):
         return common.find_item_by_key_in_list(service_type, 'type', service_list)
 
-    def _find_rate_by_target_type_uri_in_list(self, target_type_uri, rate_list):
+    def __find_rate_by_target_type_uri_in_list(self, target_type_uri, rate_list):
         return common.find_item_by_key_in_list(target_type_uri, 'targetTypeURI', rate_list)
 
-    def _find_limit_by_action_in_list(self, action_name, action_list):
+    def __find_limit_by_action_in_list(self, action_name, action_list):
         action = common.find_item_by_key_in_list(
             action_name, 'name', action_list,
             {'limit': -1}
@@ -296,26 +334,29 @@ class LimesRateLimitProvider(RateLimitProvider):
 
     def _get(self, path, params={}, headers={}):
         response_json = {}
+
+        if not params:
+            params = {}
+        if not headers:
+            headers = {}
+
         try:
-            if not params:
-                params = {}
-            if not headers:
-                headers = {}
-            self.limes_base_url = self.limes_base_url or self._get_limes_base_url()
-            if not self.limes_base_url:
-                self.logger.error("limes base path is unknown. get not get rate limits")
+            self.__limes_base_url = self.__limes_base_url or self.__get_limes_base_url()
+            if not self.__limes_base_url:
+                self.logger.error("limes base path is unknown. cannot get rate limits")
                 return None
 
-            # only list rates
-            params['rates'] = True
+            # Set X-AUTH-TOKEN header.
+            headers['X-AUTH-TOKEN'] = self.__keystone_client.session.get_token()
 
-            # set X-AUTH-TOKEN header
-            headers['X-AUTH-TOKEN'] = self.keystone.session.get_token()
-            url = self.limes_base_url + path
-
-            resp_raw = requests.get(url=url, params=params, headers=headers)
+            resp_raw = requests.get(
+                url=common.build_uri(self.__limes_base_url, path),
+                params=params,
+                headers=headers
+            )
             response_json = resp_raw.json()
         except Exception as e:
             self.logger.error("error while getting rate limits from limes: {0}".format(str(e)))
+
         finally:
             return response_json
diff --git a/rate_limit/rate_limit.py b/rate_limit/rate_limit.py
index 6644cc6..651294c 100644
--- a/rate_limit/rate_limit.py
+++ b/rate_limit/rate_limit.py
@@ -45,7 +45,7 @@ class OpenStackRateLimitMiddleware(object):
 
         # StatsD is used to emit metrics.
         statsd_host = wsgi_config.get('statsd_host', '127.0.0.1')
-        statsd_port = wsgi_config.get('statsd_port', 9125)
+        statsd_port = common.to_int(wsgi_config.get('statsd_port', 9125))
         statsd_prefix = wsgi_config.get('statsd_prefix', 'openstack_ratelimit')
 
         # Init StatsD client.
@@ -57,14 +57,13 @@ class OpenStackRateLimitMiddleware(object):
 
         # Get backend configuration.
         # Backend is used to store count of requests.
-        backend_type = wsgi_config.get('backend', 'redis')
-        self.logger.debug('using backend: {0}'.format(backend_type))
-
-        backend_host = wsgi_config.get('backend_host', '127.0.0.1')
-        self.logger.debug('using backend host {0}'.format(backend_host))
-
-        backend_port = wsgi_config.get('backend_port', '6379')
-        self.logger.debug('using backend port {0}'.format(backend_port))
+        self.backend_host = wsgi_config.get('backend_host', '127.0.0.1')
+        self.backend_port = common.to_int(self.wsgi_config.get('backend_port'), 6379)
+        self.logger.debug(
+            "using backend '{0}' on '{1}:{2}'".format('redis', self.backend_host, self.backend_port)
+        )
+        backend_timeout_seconds = common.to_int(self.wsgi_config.get('backend_timeout_seconds'), 20)
+        backend_max_connections = common.to_int(self.wsgi_config.get('backend_max_connections'), 100)
 
         # Load configuration file.
         self.config = {}
@@ -89,8 +88,8 @@ class OpenStackRateLimitMiddleware(object):
             self.cadf_service_name = common.CADF_SERVICE_TYPE_PREFIX_MAP.get(self.service_type, None)
 
         # Use configured parameters or ensure defaults.
-        max_sleep_time_seconds = int(self.wsgi_config.get(common.Constants.max_sleep_time_seconds, 20))
-        log_sleep_time_seconds = int(self.wsgi_config.get(common.Constants.log_sleep_time_seconds, 10))
+        max_sleep_time_seconds = common.to_int(self.wsgi_config.get(common.Constants.max_sleep_time_seconds), 20)
+        log_sleep_time_seconds = common.to_int(self.wsgi_config.get(common.Constants.log_sleep_time_seconds), 10)
 
         # Setup ratelimit and blacklist response.
         self._setup_response()
@@ -109,25 +108,16 @@ class OpenStackRateLimitMiddleware(object):
         # Rate limits are applied based on the tuple of (rate_limit_by, action, target_type_uri).
         self.rate_limit_by = self.wsgi_config.get('rate_limit_by', common.Constants.initiator_project_id)
 
-        # Initializes the backend as configured. Defaults to redis.
-        if backend_type == common.Constants.backend_memcache:
-            self.backend = rate_limit_backend.MemcachedBackend(
-                host=backend_host,
-                port=backend_port,
-                rate_limit_response=self.ratelimit_response,
-                max_sleep_time_seconds=max_sleep_time_seconds,
-                log_sleep_time_seconds=log_sleep_time_seconds,
-                logger=self.logger
-            )
-        else:
-            self.backend = rate_limit_backend.RedisBackend(
-                host=backend_host,
-                port=backend_port,
-                rate_limit_response=self.ratelimit_response,
-                max_sleep_time_seconds=max_sleep_time_seconds,
-                log_sleep_time_seconds=log_sleep_time_seconds,
-                logger=self.logger
-            )
+        self.backend = rate_limit_backend.RedisBackend(
+            host=self.backend_host,
+            port=self.backend_port,
+            rate_limit_response=self.ratelimit_response,
+            max_sleep_time_seconds=max_sleep_time_seconds,
+            log_sleep_time_seconds=log_sleep_time_seconds,
+            logger=self.logger,
+            timeout_seconds=backend_timeout_seconds,
+            max_connections=backend_max_connections,
+        )
 
         # Test if the backend is ready.
         is_available, msg = self.backend.is_available()
@@ -137,9 +127,7 @@ class OpenStackRateLimitMiddleware(object):
         # Provider for rate limits. Defaults to configuration file.
         # Also supports Limes.
         configuration_ratelimit_provider = provider.ConfigurationRateLimitProvider(
-            service_type=self.service_type,
-            refresh_interval_seconds=None,
-            logger=self.logger
+            service_type=self.service_type, logger=self.logger
         )
         # Force load of rate limits from configuration file.
         configuration_ratelimit_provider.read_rate_limits_from_config(config_file)
@@ -193,25 +181,21 @@ class OpenStackRateLimitMiddleware(object):
     def __setup_limes_ratelimit_provider(self):
         """Setup Limes as provider for rate limits. If not successful fallback to configuration file."""
         try:
-            limes_refresh_interval_seconds = self.wsgi_config.get(
-                'limes_refresh_interval_seconds', common.Constants.limes_refresh_interval_seconds
-            )
-
             limes_ratelimit_provider = provider.LimesRateLimitProvider(
                 service_type=self.service_type,
-                refresh_interval_seconds=limes_refresh_interval_seconds,
-                logger=self.logger
-            )
-
-            limes_ratelimit_provider.authenticate(
-                auth_url=self.wsgi_config.get('auth_url'),
+                logger=self.logger,
+                redis_host=self.backend_host,
+                redis_port=self.backend_port,
+                refresh_interval_seconds=self.wsgi_config.get(common.Constants.limes_refresh_interval_seconds, 300),
+                limes_api_uri=self.wsgi_config.get(common.Constants.limes_api_uri),
+                auth_url=self.wsgi_config.get('identity_auth_url'),
                 username=self.wsgi_config.get('username'),
                 user_domain_name=self.wsgi_config.get('user_domain_name'),
                 password=self.wsgi_config.get('password'),
                 domain_name=self.wsgi_config.get('domain_name')
             )
-
             self.ratelimit_provider = limes_ratelimit_provider
+
         except Exception as e:
             self.logger.debug("failed to setup limes rate limit provider: {0}".format(str(e)))
 
@@ -346,6 +330,7 @@ class OpenStackRateLimitMiddleware(object):
                 )
                 return
 
+            # Returns a RateLimitResponse or BlacklistResponse or None, in which case the original response is returned.
             rate_limit_response = self._rate_limit(scope, action, target_type_uri)
             if rate_limit_response:
                 rate_limit_response.set_environ(environ)
@@ -421,18 +406,20 @@ class OpenStackRateLimitMiddleware(object):
         """
         Get the scope from the requests environ.
         The scope is configurable and may be the target|initiator project uid or the initiator host address.
+        Default to initiator project ID.
 
         :param environ: the requests environ
         :return: the scope
         """
-        scope = env_scope = None
+        scope = None
         if self.rate_limit_by == common.Constants.target_project_id:
             env_scope = environ.get('WATCHER.TARGET_PROJECT_ID', None)
         elif self.rate_limit_by == common.Constants.initiator_host_address:
             env_scope = environ.get('WATCHER.INITIATOR_HOST_ADDRESS', None)
         else:
             env_scope = environ.get('WATCHER.INITIATOR_PROJECT_ID', None)
-        # make sure the scope is not 'unknown'
+
+        # Ensure the scope is not 'unknown'.
         if not common.is_none_or_unknown(env_scope):
             scope = env_scope
         return scope
@@ -444,7 +431,7 @@ class OpenStackRateLimitMiddleware(object):
 
         :param environ: the request WSGI environment
         """
-        # get service type from environ
+        # Get service type from request environ.
         if common.is_none_or_unknown(self.service_type):
             svc_type = environ.get('WATCHER.SERVICE_TYPE')
             if not common.is_none_or_unknown(svc_type):
diff --git a/requirements.txt b/requirements.txt
index 6be8b04..7cf3ba4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,7 +4,6 @@ python-keystoneclient
 keystoneauth1>=3.1.0 # Apache-2.0
 redis~=3.1.0
 requests>=2.14.2 # Apache-2.0
-python-memcached~=1.59
 six
 WebOb>=1.7.1 # MIT
 PyYAML
diff --git a/setup.py b/setup.py
index acb3951..ecff702 100644
--- a/setup.py
+++ b/setup.py
@@ -16,4 +16,8 @@ import setuptools
 
 setuptools.setup(
     setup_requires=['pbr'],
-    pbr=True)
+    pbr=True,
+    data_files=[
+        ('lua', ['rate_limit/lua/redis_sliding_window.lua'])
+    ]
+)

Burst requests
The memached backend already support burst requests. So should the redis backend.
diff --git a/rate_limit/tests/fake.py b/rate_limit/tests/fake.py
index e160923..c719941 100644
--- a/rate_limit/tests/fake.py
+++ b/rate_limit/tests/fake.py
@@ -52,6 +52,7 @@ class FakeApp(object):
 class FakeKeystoneclient(object):
     def __init__(self):
         self.session = FakeSession()
+        self.services = []
 
 
 class FakeSession(object):
diff --git a/rate_limit/tests/test_limesratelimitprovider.py b/rate_limit/tests/test_limesratelimitprovider.py
index fcf21e2..426efc5 100644
--- a/rate_limit/tests/test_limesratelimitprovider.py
+++ b/rate_limit/tests/test_limesratelimitprovider.py
@@ -31,11 +31,11 @@ class TestOpenStackRateLimitMiddlewareWithLimes(unittest.TestCase):
         limes_provider = provider.LimesRateLimitProvider(
             service_type=SERVICE_TYPE,
             refresh_interval_seconds=20,
+            keystone_client=fake.FakeKeystoneclient(),
+            limes_api_url='https://localhost:8887'
         )
-        limes_provider.keystone = fake.FakeKeystoneclient()
-        limes_provider.limes_base_url = 'https://localhost:8887'
 
-        # mock requests
+        # Mock requests to Limes.
         def _fake_get(path, params={}, headers={}):
             f = open(LIMESRATELIMITS)
             json_data = f.read()
@@ -55,14 +55,14 @@ class TestOpenStackRateLimitMiddlewareWithLimes(unittest.TestCase):
         rate_limit = self.watcher.ratelimit_provider.get_local_rate_limits(
             'abcdef1233456789', 'update', 'account/container/object'
         )
-        self.assertEqual(rate_limit, '10r/m', "the rate limit should be '10r/m'")
+        self.assertEqual(rate_limit, '10r/m', "the rate limit should be '10r/m' but got '{0}'".format(rate_limit))
 
         rate_limit = self.watcher.ratelimit_provider.get_local_rate_limits(
             '1233456789abcdef1233456789', 'delete', 'account/container'
         )
-        self.assertEqual(rate_limit, '2r/10m', "the rate limit should be '2r/10m'")
+        self.assertEqual(rate_limit, '2r/10m', "the rate limit should be '2r/10m' but got '{0}'".format(rate_limit))
 
         rate_limit = self.watcher.ratelimit_provider.get_local_rate_limits(
             'non_existent_project_id', 'delete', 'account/container'
         )
-        self.assertEqual(rate_limit, -1, "the rate limit should be '-1'")
+        self.assertEqual(rate_limit, -1, "the rate limit should be '-1' but got '{0}'".format(rate_limit))
diff --git a/rate_limit/tests/test_middleware.py b/rate_limit/tests/test_middleware.py
index 7af0b3e..bd60285 100644
--- a/rate_limit/tests/test_middleware.py
+++ b/rate_limit/tests/test_middleware.py
@@ -37,7 +37,7 @@ class TestOpenStackRateLimitMiddleware(unittest.TestCase):
             app=fake.FakeApp(),
             wsgi_config={
                 'config_file': SWIFTCONFIGPATH,
-                'max_sleep_time_seconds': 15,
+                'max_sleep_time_seconds': 20,
             }
         )
         self.is_setup = True
@@ -114,7 +114,7 @@ class TestOpenStackRateLimitMiddleware(unittest.TestCase):
             result = self.app._rate_limit(scope=scope, action=action, target_type_uri=target_type_uri)
             time.sleep(1)
             is_equal, msg = response_equal(expected[i], result)
-            self.assertTrue(is_equal, msg)
+            self.assertTrue(is_equal, "test #{0} failed: {1}".format(i, msg))
 
 
 def response_equal(expected, got):
diff --git a/rate_limit/tests/test_parse_config.py b/rate_limit/tests/test_parse_config.py
index d90dc9c..8d16b04 100644
--- a/rate_limit/tests/test_parse_config.py
+++ b/rate_limit/tests/test_parse_config.py
@@ -106,6 +106,18 @@ class TestParseConfig(unittest.TestCase):
         self.assertEqual(60, Units.parse('1m'))
         self.assertEqual(-1, Units.parse('mms'))
 
+    def test_load_lua_script(self):
+        content = common.load_lua_script('redis_sliding_window.lua')
+        self.assertIsNotNone(
+            content,
+            "the content of the lua script should not be None"
+        )
+        self.assertNotEqual(
+            content,
+            "",
+            "the content of the lua script should not be empty"
+        )
+
 
 if __name__ == '__main__':
     unittest.main()

pip install -e .
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio
apt-get update apt-get install -y gcc
3.6
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
certifi==2021.5.30
charset-normalizer==2.0.12
coverage==6.2
datadog==0.26.0
debtcollector==2.5.0
decorator==5.1.1
dnspython==2.2.1
eventlet==0.24.1
execnet==1.9.0
greenlet==2.0.2
idna==3.10
importlib-metadata==4.8.3
importlib-resources==5.4.0
iniconfig==1.1.1
iso8601==1.1.0
keystoneauth1==5.1.3
monotonic==1.6
msgpack==1.0.5
netaddr==0.10.1
netifaces==0.11.0
os-service-types==1.7.0
oslo.config==8.8.1
oslo.i18n==5.1.0
oslo.serialization==4.3.0
oslo.utils==4.13.0
packaging==21.3
pbr==6.1.1
pluggy==1.0.0
py==1.11.0
pyparsing==3.1.4
pytest==7.0.1
pytest-asyncio==0.16.0
pytest-cov==4.0.0
pytest-mock==3.6.1
pytest-xdist==3.0.2
python-keystoneclient==4.5.0
python-memcached==1.62
pytz==2025.2
PyYAML==6.0.1
-e git+https://github.com/sapcc/openstack-rate-limit-middleware.git@cc09445ada231b785fa6da441bc0a410f0c58484#egg=rate_limit_middleware
redis==3.1.0
requests==2.27.1
rfc3986==1.5.0
six==1.17.0
stevedore==3.5.2
tomli==1.2.3
typing_extensions==4.1.1
urllib3==1.26.20
WebOb==1.8.9
wrapt==1.16.0
zipp==3.6.0

name: openstack-rate-limit-middleware
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - charset-normalizer==2.0.12
      - coverage==6.2
      - datadog==0.26.0
      - debtcollector==2.5.0
      - decorator==5.1.1
      - dnspython==2.2.1
      - eventlet==0.24.1
      - execnet==1.9.0
      - greenlet==2.0.2
      - idna==3.10
      - importlib-metadata==4.8.3
      - importlib-resources==5.4.0
      - iniconfig==1.1.1
      - iso8601==1.1.0
      - keystoneauth1==5.1.3
      - monotonic==1.6
      - msgpack==1.0.5
      - netaddr==0.10.1
      - netifaces==0.11.0
      - os-service-types==1.7.0
      - oslo-config==8.8.1
      - oslo-i18n==5.1.0
      - oslo-serialization==4.3.0
      - oslo-utils==4.13.0
      - packaging==21.3
      - pbr==6.1.1
      - pluggy==1.0.0
      - py==1.11.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytest-asyncio==0.16.0
      - pytest-cov==4.0.0
      - pytest-mock==3.6.1
      - pytest-xdist==3.0.2
      - python-keystoneclient==4.5.0
      - python-memcached==1.62
      - pytz==2025.2
      - pyyaml==6.0.1
      - redis==3.1.0
      - requests==2.27.1
      - rfc3986==1.5.0
      - six==1.17.0
      - stevedore==3.5.2
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - webob==1.8.9
      - wrapt==1.16.0
      - zipp==3.6.0
prefix: /opt/conda/envs/openstack-rate-limit-middleware

rate_limit/tests/test_parse_config.py::TestParseConfig::test_load_lua_script
rate_limit/tests/test_middleware.py::TestOpenStackRateLimitMiddleware::test_is_ratelimited_swift_local_container_update
rate_limit/tests/test_limesratelimitprovider.py::TestOpenStackRateLimitMiddlewareWithLimes::test_list_ratelimits_for_projects_in_domain rate_limit/tests/test_middleware.py::TestOpenStackRateLimitMiddleware::test_get_rate_limit rate_limit/tests/test_middleware.py::TestOpenStackRateLimitMiddleware::test_is_scope_blacklisted rate_limit/tests/test_middleware.py::TestOpenStackRateLimitMiddleware::test_is_scope_whitelisted rate_limit/tests/test_parse_config.py::TestParseConfig::test_load_swift_config rate_limit/tests/test_parse_config.py::TestParseConfig::test_parse_and_convert_to_per_seconds rate_limit/tests/test_parse_config.py::TestParseConfig::test_parse_and_convert_unit
rate_limit/rate_limit.py setup.py rate_limit/errors.py docs/install.md ChangeLog docs/user.md rate_limit/backend.py README.md rate_limit/provider.py docs/configure.md rate_limit/common.py rate_limit/lua/redis_sliding_window.lua docs/draft_limes.md requirements.txt
rate_limit/rate_limit.py setup.py rate_limit/errors.py docs/install.md ChangeLog docs/user.md rate_limit/backend.py README.md rate_limit/provider.py docs/configure.md rate_limit/common.py rate_limit/lua/redis_sliding_window.lua docs/draft_limes.md requirements.txt