Repository: avenga/couper
Pull request #523 (2022-06-17 08:44:51)

Rate limit
Describe your changes!

---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>


malud: @alex-schneider  Rebased. Documentation: docs/website/content/configuration/block/rate_limit.md

Patch:
diff --git a/config/backend.go b/config/backend.go
index 6bf6e20a..77be35d5 100644
--- a/config/backend.go
+++ b/config/backend.go
@@ -16,14 +16,15 @@ var (
 
 // Backend represents the <Backend> object.
 type Backend struct {
-	DisableCertValidation  bool     `hcl:"disable_certificate_validation,optional" docs:"Disables the peer certificate validation."`
-	DisableConnectionReuse bool     `hcl:"disable_connection_reuse,optional" docs:"Disables reusage of connections to the origin."`
-	Health                 *Health  `hcl:"beta_health,block"`
-	HTTP2                  bool     `hcl:"http2,optional" docs:"Enables the HTTP2 support."`
-	MaxConnections         int      `hcl:"max_connections,optional" docs:"The maximum number of concurrent connections in any state (_active_ or _idle_) to the origin." default:"0"`
-	Name                   string   `hcl:"name,label,optional"`
-	OpenAPI                *OpenAPI `hcl:"openapi,block"`
-	Remain                 hcl.Body `hcl:",remain"`
+	DisableCertValidation  bool       `hcl:"disable_certificate_validation,optional" docs:"Disables the peer certificate validation."`
+	DisableConnectionReuse bool       `hcl:"disable_connection_reuse,optional" docs:"Disables reusage of connections to the origin."`
+	Health                 *Health    `hcl:"beta_health,block"`
+	HTTP2                  bool       `hcl:"http2,optional" docs:"Enables the HTTP2 support."`
+	MaxConnections         int        `hcl:"max_connections,optional" docs:"The maximum number of concurrent connections in any state (_active_ or _idle_) to the origin." default:"0"`
+	Name                   string     `hcl:"name,label,optional"`
+	OpenAPI                *OpenAPI   `hcl:"openapi,block"`
+	RateLimits             RateLimits `hcl:"beta_rate_limit,block"`
+	Remain                 hcl.Body   `hcl:",remain"`
 }
 
 // Reference implements the <BackendReference> interface.
diff --git a/config/rate_limit.go b/config/rate_limit.go
new file mode 100644
index 00000000..1fb639df
--- /dev/null
+++ b/config/rate_limit.go
@@ -0,0 +1,12 @@
+package config
+
+// RateLimit represents the <config.RateLimit> object.
+type RateLimit struct {
+	Mode         string `hcl:"mode,optional"`
+	Period       string `hcl:"period"`
+	PerPeriod    uint   `hcl:"per_period"`
+	PeriodWindow string `hcl:"period_window,optional"`
+}
+
+// RateLimits represents a list of <config.RateLimits> objects.
+type RateLimits []*RateLimit
diff --git a/config/request/context_key.go b/config/request/context_key.go
index 2af6ac0f..22ea9b55 100644
--- a/config/request/context_key.go
+++ b/config/request/context_key.go
@@ -13,6 +13,7 @@ const (
 	BetaGrantedPermissions
 	BetaRequiredPermission
 	BufferOptions
+	ConfigDryRun
 	ConnectTimeout
 	ContextVariablesSynced
 	Endpoint
diff --git a/config/runtime/backend.go b/config/runtime/backend.go
index ffe52ba4..579051d8 100644
--- a/config/runtime/backend.go
+++ b/config/runtime/backend.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"math"
 	"net/http"
+	"strings"
 
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/gohcl"
@@ -16,6 +17,7 @@ import (
 	hclbody "github.com/avenga/couper/config/body"
 	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/eval"
+	"github.com/avenga/couper/handler/ratelimit"
 	"github.com/avenga/couper/handler/transport"
 	"github.com/avenga/couper/handler/validation"
 )
@@ -72,6 +74,18 @@ func newBackend(evalCtx *hcl.EvalContext, backendCtx hcl.Body, log *logrus.Entry
 		MaxConnections:         beConf.MaxConnections,
 	}
 
+	if len(beConf.RateLimits) > 0 {
+		if strings.HasPrefix(beConf.Name, "anonymous_") {
+			return nil, fmt.Errorf("anonymous backend (%q) cannot define 'beta_rate_limit' block(s)", beConf.Name)
+		}
+
+		rateLimits, err := ratelimit.ConfigureRateLimits(conf.Context, beConf.RateLimits, log)
+		if err != nil {
+			return nil, err
+		}
+		tc.RateLimits = rateLimits
+	}
+
 	options := &transport.BackendOptions{}
 	var err error
 	options.OpenAPI, err = validation.NewOpenAPIOptions(beConf.OpenAPI)
diff --git a/docs/website/content/2.configuration/4.block/backend.md b/docs/website/content/2.configuration/4.block/backend.md
index 14df717b..bba29d5b 100644
--- a/docs/website/content/2.configuration/4.block/backend.md
+++ b/docs/website/content/2.configuration/4.block/backend.md
@@ -12,7 +12,7 @@ Backends can be defined in the [Definitions Block](definitions) and referenced b
 
 | Block name | Context                                                                                                                                                                                                                                   | Label                                                                     | Nested block(s)                                                                                                                       |
 |:-----------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------|
-| `backend`  | [Definitions Block](definitions), [Proxy Block](proxy), [Request Block](request), [OAuth2 CC Block](oauth2req_auth), [JWT Block](jwt), [OAuth2 AC Block (beta)](oauth2), [OIDC Block](oidc)                                               | &#9888; required, when defined in [Definitions Block](definitions)        | [OpenAPI Block](openapi), [OAuth2 CC Block](oauth2req_auth), [Health Block](health), [Token Request (Beta) Block](beta_token_request) |
+| `backend`  | [Definitions Block](definitions), [Proxy Block](proxy), [Request Block](request), [OAuth2 CC Block](oauth2req_auth), [JWT Block](jwt), [OAuth2 AC Block (beta)](oauth2), [OIDC Block](oidc)                                               | &#9888; required, when defined in [Definitions Block](definitions)        | [OpenAPI Block](openapi), [OAuth2 CC Block](oauth2req_auth), [Health Block](health), [Token Request (Beta) Block](beta_token_request), [Rate Limit Block (beta)](rate_limit) |
 
 ::attributes
 ---
diff --git a/docs/website/content/2.configuration/4.block/rate_limit.md b/docs/website/content/2.configuration/4.block/rate_limit.md
new file mode 100644
index 00000000..0b09ba0e
--- /dev/null
+++ b/docs/website/content/2.configuration/4.block/rate_limit.md
@@ -0,0 +1,18 @@
+# Rate Limit (Beta)
+
+Rate limiting protects backend services. It implements quota management used to avoid cascading failures or to spare resources.
+
+| Block name        | Context                         | Label | Nested block(s) |
+| :---------------- | :------------------------------ | :---- | :-------------- |
+| `beta_rate_limit` | named [`backend` block](backend)| -     | -               |
+
+| Attribute(s)    | Type                  | Default | Description | Characteristic(s) | Example |
+| :-------------- | :-------------------- | :--------------- | :--------------- | :--------------- | :--------------- |
+| `period`        | [duration](#duration) | - | Defines the rate limit period. | &#9888; required, must not be negative or `0` (zero) | `period = "1m"` |
+| `per_period`    | integer               | - | Defines the number of allowed backend requests in a period. | &#9888; required, must not be negative or `0` (zero) | `per_period = 100` |
+| `period_window` | string                | `"sliding"` | Defines the window of the period. A `fixed` window permits `per_period` requests within `period` after the first request to the parent backend. After the `period` has expired, another `per_period` request is permitted. The `sliding` window ensures that only `per_period` requests are sent in any interval of length `period`. | Allowed values: `"fixed"` or `"sliding"` | `period_window = "sliding"` |
+| `mode`          | string                | `"wait"` | If `mode` is set to `block` and the rate limit is exceeded, the client request is immediately answered with HTTP status code `429` (Too Many Requests) and no backend request is made. If `mode` is set to `wait` and the rate limit is exceeded, the request waits for the next free rate limiting period. | Allowed values: `"wait"` or `"block"` | `mode = "wait"` |
+
+**Note:** Anonymous backends (inline backends without label) cannot define `beta_rate_limit` block(s).
+
+::duration
diff --git a/docs/website/content/2.configuration/7.error-handling.md b/docs/website/content/2.configuration/7.error-handling.md
index f412dfc9..ae01f654 100644
--- a/docs/website/content/2.configuration/7.error-handling.md
+++ b/docs/website/content/2.configuration/7.error-handling.md
@@ -70,6 +70,7 @@ The following table documents error types that can be handled in `endpoint` bloc
 | `backend_unhealthy` (`backend`)                    | A backend is unhealthy and will not send the request                                                    | Send error template with status `502`.                                                                        |
 | `beta_backend_token_request` (`backend`)           | A token request for the backend has failed                                                              | Send error template with status `502`.                                                                        |
 | `access_control`                                   | Access control related errors                                                                           | Send error template with status `403`.                                                                        |
+| `beta_backend_rate_limit_exceeded`                 | Backend rate limit related errors                                                                       | Send error template with status `429`.                                                                        |
 | `beta_insufficient_permissions` (`access_control`) | The permission required for the requested operation is not in the permissions granted to the requester. | Send error template with status `403`.                                                                        |
 | `endpoint`                                         | All catchable `endpoint` related errors                                                                 | Send error template with status `502`.                                                                        |
 | `sequence` (`endpoint`)                            | A `request` or `proxy` block request has been failed while depending on another one                     | Send error template with status `502`.                                                                        |
diff --git a/errors/type_defintions.go b/errors/type_defintions.go
index 556365a8..0c467c56 100644
--- a/errors/type_defintions.go
+++ b/errors/type_defintions.go
@@ -24,6 +24,7 @@ var Definitions = []*Error{
 
 	Backend,
 	Backend.Kind("backend_openapi_validation").Status(http.StatusBadRequest),
+	Backend.Kind("beta_backend_rate_limit_exceeded").Status(http.StatusTooManyRequests),
 	Backend.Kind("backend_timeout").Status(http.StatusGatewayTimeout),
 	Backend.Kind("beta_backend_token_request"),
 	Backend.Kind("backend_unhealthy"),
diff --git a/errors/types_generated.go b/errors/types_generated.go
index b4f29cdc..9bbdc8fb 100644
--- a/errors/types_generated.go
+++ b/errors/types_generated.go
@@ -3,22 +3,23 @@
 package errors
 
 var (
-	BasicAuth                   = Definitions[1]
-	BasicAuthCredentialsMissing = Definitions[2]
-	Jwt                         = Definitions[3]
-	JwtTokenExpired             = Definitions[4]
-	JwtTokenInvalid             = Definitions[5]
-	JwtTokenMissing             = Definitions[6]
-	Oauth2                      = Definitions[7]
-	Saml2                       = Definitions[8]
-	Saml                        = Definitions[9]
-	BetaInsufficientPermissions = Definitions[10]
-	BackendOpenapiValidation    = Definitions[12]
-	BackendTimeout              = Definitions[13]
-	BetaBackendTokenRequest     = Definitions[14]
-	BackendUnhealthy            = Definitions[15]
-	Sequence                    = Definitions[17]
-	UnexpectedStatus            = Definitions[18]
+	BasicAuth                    = Definitions[1]
+	BasicAuthCredentialsMissing  = Definitions[2]
+	Jwt                          = Definitions[3]
+	JwtTokenExpired              = Definitions[4]
+	JwtTokenInvalid              = Definitions[5]
+	JwtTokenMissing              = Definitions[6]
+	Oauth2                       = Definitions[7]
+	Saml2                        = Definitions[8]
+	Saml                         = Definitions[9]
+	BetaInsufficientPermissions  = Definitions[10]
+	BackendOpenapiValidation     = Definitions[12]
+	BetaBackendRateLimitExceeded = Definitions[13]
+	BackendTimeout               = Definitions[14]
+	BetaBackendTokenRequest      = Definitions[15]
+	BackendUnhealthy             = Definitions[16]
+	Sequence                     = Definitions[18]
+	UnexpectedStatus             = Definitions[19]
 )
 
 // typeDefinitions holds all related error definitions which are
@@ -28,25 +29,26 @@ type typeDefinitions map[string]*Error
 // types holds all implemented ones. The name must match the structs
 // snake-name for fallback purposes. See TypeToSnake usage and reference.
 var types = typeDefinitions{
-	"access_control":                 AccessControl,
-	"basic_auth":                     BasicAuth,
-	"basic_auth_credentials_missing": BasicAuthCredentialsMissing,
-	"jwt":                            Jwt,
-	"jwt_token_expired":              JwtTokenExpired,
-	"jwt_token_invalid":              JwtTokenInvalid,
-	"jwt_token_missing":              JwtTokenMissing,
-	"oauth2":                         Oauth2,
-	"saml2":                          Saml2,
-	"saml":                           Saml,
-	"beta_insufficient_permissions":  BetaInsufficientPermissions,
-	"backend":                        Backend,
-	"backend_openapi_validation":     BackendOpenapiValidation,
-	"backend_timeout":                BackendTimeout,
-	"beta_backend_token_request":     BetaBackendTokenRequest,
-	"backend_unhealthy":              BackendUnhealthy,
-	"endpoint":                       Endpoint,
-	"sequence":                       Sequence,
-	"unexpected_status":              UnexpectedStatus,
+	"access_control":                   AccessControl,
+	"basic_auth":                       BasicAuth,
+	"basic_auth_credentials_missing":   BasicAuthCredentialsMissing,
+	"jwt":                              Jwt,
+	"jwt_token_expired":                JwtTokenExpired,
+	"jwt_token_invalid":                JwtTokenInvalid,
+	"jwt_token_missing":                JwtTokenMissing,
+	"oauth2":                           Oauth2,
+	"saml2":                            Saml2,
+	"saml":                             Saml,
+	"beta_insufficient_permissions":    BetaInsufficientPermissions,
+	"backend":                          Backend,
+	"backend_openapi_validation":       BackendOpenapiValidation,
+	"beta_backend_rate_limit_exceeded": BetaBackendRateLimitExceeded,
+	"backend_timeout":                  BackendTimeout,
+	"beta_backend_token_request":       BetaBackendTokenRequest,
+	"backend_unhealthy":                BackendUnhealthy,
+	"endpoint":                         Endpoint,
+	"sequence":                         Sequence,
+	"unexpected_status":                UnexpectedStatus,
 }
 
 // IsKnown tells the configuration callee if Couper
@@ -58,4 +60,4 @@ func IsKnown(errorType string) bool {
 
 // SuperTypesMapsByContext holds maps for error super-types to sub-types
 // by a given context block type (e.g. api or endpoint).
-var SuperTypesMapsByContext = map[string]map[string][]string{"api": map[string][]string{"*": []string{"beta_insufficient_permissions", "backend_openapi_validation", "backend_timeout", "beta_backend_token_request", "backend_unhealthy"}, "access_control": []string{"beta_insufficient_permissions"}, "backend": []string{"backend_openapi_validation", "backend_timeout", "beta_backend_token_request", "backend_unhealthy"}}, "endpoint": map[string][]string{"*": []string{"beta_insufficient_permissions", "backend_openapi_validation", "backend_timeout", "beta_backend_token_request", "backend_unhealthy", "sequence", "unexpected_status"}, "access_control": []string{"beta_insufficient_permissions"}, "backend": []string{"backend_openapi_validation", "backend_timeout", "beta_backend_token_request", "backend_unhealthy"}, "endpoint": []string{"sequence", "unexpected_status"}}}
+var SuperTypesMapsByContext = map[string]map[string][]string{"api": map[string][]string{"*": []string{"beta_insufficient_permissions", "backend_openapi_validation", "beta_backend_rate_limit_exceeded", "backend_timeout", "beta_backend_token_request", "backend_unhealthy"}, "access_control": []string{"beta_insufficient_permissions"}, "backend": []string{"backend_openapi_validation", "beta_backend_rate_limit_exceeded", "backend_timeout", "beta_backend_token_request", "backend_unhealthy"}}, "endpoint": map[string][]string{"*": []string{"beta_insufficient_permissions", "backend_openapi_validation", "beta_backend_rate_limit_exceeded", "backend_timeout", "beta_backend_token_request", "backend_unhealthy", "sequence", "unexpected_status"}, "access_control": []string{"beta_insufficient_permissions"}, "backend": []string{"backend_openapi_validation", "beta_backend_rate_limit_exceeded", "backend_timeout", "beta_backend_token_request", "backend_unhealthy"}, "endpoint": []string{"sequence", "unexpected_status"}}}
diff --git a/handler/ratelimit/config.go b/handler/ratelimit/config.go
new file mode 100644
index 00000000..f7bab616
--- /dev/null
+++ b/handler/ratelimit/config.go
@@ -0,0 +1,118 @@
+package ratelimit
+
+import (
+	"context"
+	"fmt"
+	"sort"
+	"time"
+
+	"github.com/avenga/couper/config"
+	"github.com/sirupsen/logrus"
+)
+
+const (
+	dummy = iota
+	modeBlock
+	modeWait
+	windowFixed
+	windowSliding
+)
+
+type RateLimit struct {
+	count       uint
+	logger      *logrus.Entry
+	mode        int
+	period      time.Duration
+	periodStart time.Time
+	perPeriod   uint
+	ringBuffer  *ringBuffer
+	window      int
+	quitCh      <-chan struct{}
+}
+
+type RateLimits []*RateLimit
+
+func ConfigureRateLimits(ctx context.Context, limits config.RateLimits, logger *logrus.Entry) (RateLimits, error) {
+	var (
+		mode       int
+		rateLimits RateLimits
+		window     int
+	)
+
+	uniqueDurations := make(map[time.Duration]struct{})
+
+	for _, limit := range limits {
+		d, err := config.ParseDuration("period", limit.Period, 0)
+		if err != nil {
+			return nil, err
+		}
+
+		if d == 0 {
+			return nil, fmt.Errorf("'period' must not be 0 (zero)")
+		}
+		if limit.PerPeriod == 0 {
+			return nil, fmt.Errorf("'per_period' must not be 0 (zero)")
+		}
+
+		if _, ok := uniqueDurations[time.Duration(d.Nanoseconds())]; ok {
+			return nil, fmt.Errorf("duplicate period (%q) found", limit.Period)
+		}
+
+		uniqueDurations[time.Duration(d.Nanoseconds())] = struct{}{}
+
+		switch limit.PeriodWindow {
+		case "":
+			fallthrough
+		case "sliding":
+			window = windowSliding
+		case "fixed":
+			window = windowFixed
+		default:
+			return nil, fmt.Errorf("unsupported 'period_window' (%q) given", limit.PeriodWindow)
+		}
+
+		switch limit.Mode {
+		case "":
+			fallthrough
+		case "wait":
+			mode = modeWait
+		case "block":
+			mode = modeBlock
+		default:
+			return nil, fmt.Errorf("unsupported 'mode' (%q) given", limit.Mode)
+		}
+
+		rateLimit := &RateLimit{
+			logger:    logger,
+			mode:      mode,
+			period:    time.Duration(d.Nanoseconds()),
+			perPeriod: limit.PerPeriod,
+			window:    window,
+			quitCh:    ctx.Done(),
+		}
+
+		switch rateLimit.window {
+		case windowSliding:
+			rateLimit.ringBuffer = newRingBuffer(rateLimit.perPeriod)
+		}
+
+		rateLimits = append(rateLimits, rateLimit)
+	}
+
+	// Sort 'rateLimits' by 'period' DESC.
+	sort.Slice(rateLimits, func(i, j int) bool {
+		return rateLimits[i].period > rateLimits[j].period
+	})
+
+	return rateLimits, nil
+}
+
+// countRequest MUST only be called after checkCapacity()
+func (rl *RateLimit) countRequest() {
+	switch rl.window {
+	case windowFixed:
+		rl.count++
+	case windowSliding:
+		rl.ringBuffer.put(time.Now())
+	}
+}
diff --git a/handler/ratelimit/limiter.go b/handler/ratelimit/limiter.go
new file mode 100644
index 00000000..00f2e46b
--- /dev/null
+++ b/handler/ratelimit/limiter.go
@@ -0,0 +1,170 @@
+package ratelimit
+
+import (
+	"net/http"
+	"runtime/debug"
+	"sync"
+	"time"
+
+	"github.com/avenga/couper/errors"
+)
+
+type Limiter struct {
+	check     chan *slowTrip
+	limits    RateLimits
+	mu        sync.RWMutex
+	transport http.RoundTripper
+}
+
+type slowTrip struct {
+	err    error
+	out    chan *slowTrip
+	quitCh <-chan struct{}
+	req    *http.Request
+	res    *http.Response
+}
+
+func NewLimiter(transport http.RoundTripper, limits RateLimits) *Limiter {
+	if len(limits) == 0 {
+		return nil
+	}
+
+	limiter := &Limiter{
+		check:     make(chan *slowTrip),
+		limits:    limits,
+		transport: transport,
+	}
+
+	for _, rl := range limits {
+		// Init the start of a period.
+		rl.periodStart = time.Now()
+	}
+
+	go limiter.slowTripper()
+
+	return limiter
+}
+
+func (l *Limiter) RoundTrip(req *http.Request) (*http.Response, error) {
+	outCh := make(chan *slowTrip)
+
+	trip := &slowTrip{
+		out:    outCh,
+		quitCh: l.limits[0].quitCh,
+		req:    req,
+	}
+
+	select {
+	case l.check <- trip:
+	case <-req.Context().Done():
+		return nil, req.Context().Err()
+	}
+
+	trip = <-outCh
+
+	return trip.res, trip.err
+}
+
+func (l *Limiter) slowTripper() {
+	defer func() {
+		if rc := recover(); rc != nil {
+			l.limits[0].logger.WithField("panic", string(debug.Stack())).Panic(rc)
+		}
+	}()
+
+	for {
+		select {
+		case <-l.limits[0].quitCh:
+			return
+		case trip := <-l.check:
+			select {
+			case <-trip.req.Context().Done():
+				// The request was canceled while in the queue.
+				trip.err = trip.req.Context().Err()
+				trip.out <- trip
+
+				// Do not sleep for X canceled requests.
+				continue
+			default:
+			}
+
+			l.mu.Lock()
+
+			if mode, timeToWait := l.checkCapacity(); mode == modeBlock && timeToWait > 0 {
+				// We do not wait, we want block directly.
+				trip.err = errors.BetaBackendRateLimitExceeded
+				trip.out <- trip
+
+				l.mu.Unlock()
+			} else {
+				select {
+				// Noop if 'timeToWait' is 0.
+				case <-time.After(timeToWait):
+				case <-trip.req.Context().Done():
+					// The request was canceled while in the queue.
+					trip.err = trip.req.Context().Err()
+					trip.out <- trip
+
+					// Do not sleep for X canceled requests.
+					continue
+				}
+
+				l.countRequest()
+
+				l.mu.Unlock()
+
+				// Do not wait for the response...
+				go func() {
+					trip.res, trip.err = l.transport.RoundTrip(trip.req)
+
+					if trip.res != nil && trip.res.StatusCode == http.StatusTooManyRequests {
+						trip.err = errors.BetaBackendRateLimitExceeded.With(trip.err)
+					}
+
+					trip.out <- trip
+				}()
+			}
+		}
+	}
+}
+
+func (l *Limiter) checkCapacity() (mode int, t time.Duration) {
+	now := time.Now()
+
+	for _, rl := range l.limits {
+		switch rl.window {
+		case windowFixed:
+			// Update current period.
+			multiplicator := ((now.UnixNano() - rl.periodStart.UnixNano()) / int64(time.Nanosecond)) / rl.period.Nanoseconds()
+			if multiplicator > 0 {
+				rl.periodStart = rl.periodStart.Add(time.Duration(rl.period.Nanoseconds() * multiplicator))
+				rl.count = 0
+			}
+
+			if rl.count >= rl.perPeriod {
+				// Calculate the 'timeToWait'.
+				t = time.Duration((rl.periodStart.Add(rl.period).UnixNano() - now.UnixNano()) / int64(time.Nanosecond))
+
+				mode = rl.mode
+			}
+		case windowSliding:
+			latest := rl.ringBuffer.get()
+
+			if !latest.IsZero() && latest.Add(rl.period).After(now) {
+				// Calculate the 'timeToWait'.
+				t = time.Duration((latest.Add(rl.period).UnixNano() - now.UnixNano()) / int64(time.Nanosecond))
+
+				mode = rl.mode
+			}
+		}
+	}
+
+	return
+}
+
+// countRequest MUST only be called after checkCapacity()
+func (l *Limiter) countRequest() {
+	for _, rl := range l.limits {
+		rl.countRequest()
+	}
+}
diff --git a/handler/ratelimit/ring_buffer.go b/handler/ratelimit/ring_buffer.go
new file mode 100644
index 00000000..16df0689
--- /dev/null
+++ b/handler/ratelimit/ring_buffer.go
@@ -0,0 +1,58 @@
+package ratelimit
+
+import (
+	"sync"
+	"time"
+)
+
+type ringBuffer struct {
+	buf []time.Time
+	len uint
+	mu  sync.RWMutex
+	r   uint
+	w   uint
+}
+
+// newRingBuffer creates a new ringBuffer
+// instance. ringBuffer is thread safe.
+func newRingBuffer(len uint) *ringBuffer {
+	return &ringBuffer{
+		buf: make([]time.Time, len),
+		len: len,
+		r:   0,
+		w:   len - 1,
+	}
+}
+
+// put rotates the ring buffer and puts t at
+// the "last" position. r must not be empty.
+func (r *ringBuffer) put(t time.Time) {
+	if r == nil {
+		panic("r must not be empty")
+	}
+
+	r.mu.Lock()
+
+	r.r++
+	r.r %= r.len
+
+	r.w++
+	r.w %= r.len
+
+	r.buf[r.w] = t
+
+	r.mu.Unlock()
+}
+
+// get returns the value of the "first" element
+// in the ring buffer. r must not be empty.
+func (r *ringBuffer) get() time.Time {
+	if r == nil {
+		panic("r must not be empty")
+	}
+
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	return r.buf[r.r]
+}
diff --git a/handler/transport/backend.go b/handler/transport/backend.go
index 7c6f79b8..8ab3a704 100644
--- a/handler/transport/backend.go
+++ b/handler/transport/backend.go
@@ -26,6 +26,7 @@ import (
 	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/eval"
+	"github.com/avenga/couper/handler/ratelimit"
 	"github.com/avenga/couper/handler/validation"
 	"github.com/avenga/couper/internal/seetie"
 	"github.com/avenga/couper/logging"
@@ -42,7 +43,6 @@ var (
 	_ seetie.Object     = &Backend{}
 )
 
-// Backend represents the transport configuration.
 type Backend struct {
 	context             hcl.Body
 	healthInfo          *HealthInfo
@@ -51,7 +51,7 @@ type Backend struct {
 	name                string
 	openAPIValidator    *validation.OpenAPI
 	requestAuthorizer   []RequestAuthorizer
-	transport           *http.Transport
+	transport           http.RoundTripper
 	transportConf       *Config
 	transportConfResult Config
 	transportOnce       sync.Once
@@ -92,9 +92,14 @@ func NewBackend(ctx hcl.Body, tc *Config, opts *BackendOptions, log *logrus.Entr
 	return backend.upstreamLog
 }
 
-// initOnce ensures synced transport configuration. First request will setup the origin, hostname and tls.
+// initOnce ensures synced transport configuration. First request will setup the rate limits, origin, hostname and tls.
 func (b *Backend) initOnce(conf *Config) {
-	b.transport = NewTransport(conf, b.logEntry)
+	if len(b.transportConf.RateLimits) > 0 {
+		b.transport = ratelimit.NewLimiter(NewTransport(conf, b.logEntry), b.transportConf.RateLimits)
+	} else {
+		b.transport = NewTransport(conf, b.logEntry)
+	}
+
 	b.healthyMu.Lock()
 	b.transportConfResult = *conf
 	healthy := b.healthInfo.Healthy
@@ -313,6 +318,10 @@ func (b *Backend) innerRoundTrip(req *http.Request, tc *Config, deadlineErr <-ch
 				return nil, derr
 			}
 		default:
+			if _, ok := err.(*errors.Error); ok {
+				return nil, err
+			}
+
 			return nil, errors.Backend.Label(b.name).With(err)
 		}
 	}
diff --git a/handler/transport/probe.go b/handler/transport/probe.go
index a4db28e7..54004673 100644
--- a/handler/transport/probe.go
+++ b/handler/transport/probe.go
@@ -97,7 +97,7 @@ func NewProbe(log *logrus.Entry, tc *Config, opts *config.HealthCheck, listener
 	}
 
 	// do not start go-routine on config check (-watch)
-	if _, exist := opts.Context.Value("config-dry-run").(bool); exist {
+	if _, exist := opts.Context.Value(request.ConfigDryRun).(bool); exist {
 		return
 	}
 	go p.probe(opts.Context)
diff --git a/handler/transport/transport.go b/handler/transport/transport.go
index 45c78e0c..c5b82b9a 100644
--- a/handler/transport/transport.go
+++ b/handler/transport/transport.go
@@ -16,6 +16,7 @@ import (
 
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/request"
+	"github.com/avenga/couper/handler/ratelimit"
 	"github.com/avenga/couper/telemetry"
 	"golang.org/x/net/http/httpproxy"
 )
@@ -30,6 +31,7 @@ type Config struct {
 	MaxConnections         int
 	NoProxyFromEnv         bool
 	Proxy                  string
+	RateLimits             ratelimit.RateLimits
 
 	ConnectTimeout time.Duration
 	TTFBTimeout    time.Duration
diff --git a/internal/seetie/convert.go b/internal/seetie/convert.go
index a1ee5a59..b0c43b81 100644
--- a/internal/seetie/convert.go
+++ b/internal/seetie/convert.go
@@ -90,7 +90,7 @@ func ValuesMapToValue(m url.Values) cty.Value {
 }
 
 func stringListToValue(l []string) cty.Value {
-	if l == nil || len(l) == 0 {
+	if len(l) == 0 {
 		return cty.ListValEmpty(cty.String)
 	}
 
diff --git a/main.go b/main.go
index 02dd3694..2f191ba6 100644
--- a/main.go
+++ b/main.go
@@ -25,6 +25,7 @@ import (
 	"github.com/avenga/couper/config/configload"
 	"github.com/avenga/couper/config/configload/file"
 	"github.com/avenga/couper/config/env"
+	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/config/runtime"
 	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/logging"
@@ -227,7 +228,7 @@ func realmain(ctx context.Context, arguments []string) int {
 			tmpMemStore := cache.New(logger, tmpStoreCh)
 
 			dryCtx, cancelDry := context.
-				WithCancel(context.WithValue(context.Background(), "config-dry-run", true))
+				WithCancel(context.WithValue(context.Background(), request.ConfigDryRun, true))
 			cf.Context = cf.Context.(*eval.Context).WithContext(dryCtx)
 
 			_, reloadErr = runtime.NewServerConfiguration(cf, logger.WithFields(fields), tmpMemStore)


Test patch:
diff --git a/config/configload/load_test.go b/config/configload/load_test.go
index 1cd3df7b..83bae663 100644
--- a/config/configload/load_test.go
+++ b/config/configload/load_test.go
@@ -157,6 +157,67 @@ func TestHealthCheck(t *testing.T) {
 	}
 }
 
+func TestRateLimit(t *testing.T) {
+	tests := []struct {
+		name  string
+		hcl   string
+		error string
+	}{
+		{
+			"missing per_period",
+			``,
+			`Missing required argument; The argument "per_period" is required`,
+		},
+		{
+			"missing period",
+			`per_period = 10`,
+			`Missing required argument; The argument "period" is required`,
+		},
+		{
+			"OK",
+			`period = "1m"
+			 per_period = 10`,
+			"",
+		},
+	}
+
+	logger, _ := test.NewLogger()
+	log := logger.WithContext(context.TODO())
+
+	template := `
+		server {}
+		definitions {
+		  backend "foo" {
+		    beta_rate_limit {
+		      %s
+		    }
+		  }
+		}`
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(subT *testing.T) {
+			conf, err := configload.LoadBytes([]byte(fmt.Sprintf(template, tt.hcl)), "couper.hcl")
+
+			closeCh := make(chan struct{})
+			defer close(closeCh)
+			memStore := cache.New(log, closeCh)
+
+			if conf != nil {
+				_, err = runtime.NewServerConfiguration(conf, log, memStore)
+			}
+
+			var errorMsg = ""
+			if err != nil {
+				errorMsg = err.Error()
+			}
+
+			if !strings.Contains(errorMsg, tt.error) {
+				subT.Errorf("%q: Unexpected configuration error:\n\tWant: %q\n\tGot:  %q", tt.name, tt.error, errorMsg)
+			}
+		})
+	}
+}
+
 func TestEndpointPaths(t *testing.T) {
 	tests := []struct {
 		name       string
diff --git a/handler/ratelimit/config_internal_test.go b/handler/ratelimit/config_internal_test.go
new file mode 100644
index 00000000..bc5b3f0b
--- /dev/null
+++ b/handler/ratelimit/config_internal_test.go
@@ -0,0 +1,92 @@
+package ratelimit
+
+import (
+	"context"
+	"testing"
+
+	"github.com/avenga/couper/config"
+)
+
+func TestConfig_Errors(t *testing.T) {
+	type testCase struct {
+		configured config.RateLimits
+		expMessage string
+	}
+
+	var (
+		foo     string = "foo"
+		min     string = "1m"
+		sec     string = "60s"
+		neg     string = "-1s"
+		num     uint   = 123
+		zeroStr string = "0s"
+		zeroInt uint   = 0
+	)
+
+	for _, tc := range []testCase{
+		{
+			[]*config.RateLimit{
+				{PerPeriod: num, PeriodWindow: ""},
+			},
+			"'period' must not be 0 (zero)",
+		},
+		{
+			[]*config.RateLimit{
+				{Period: min, PeriodWindow: ""},
+			},
+			"'per_period' must not be 0 (zero)",
+		},
+		{
+			[]*config.RateLimit{
+				{Period: foo, PerPeriod: num, PeriodWindow: ""},
+			},
+			`period: time: invalid duration "foo"`,
+		},
+		{
+			[]*config.RateLimit{
+				{Period: neg, PerPeriod: num, PeriodWindow: ""},
+			},
+			`period: cannot be negative: "-1s"`,
+		},
+		{
+			[]*config.RateLimit{
+				{Period: zeroStr, PerPeriod: num, PeriodWindow: ""},
+			},
+			`'period' must not be 0 (zero)`,
+		},
+		{
+			[]*config.RateLimit{
+				{Period: min, PerPeriod: zeroInt, PeriodWindow: ""},
+			},
+			`'per_period' must not be 0 (zero)`,
+		},
+		{
+			[]*config.RateLimit{
+				{Period: min, PerPeriod: num, PeriodWindow: ""},
+				{Period: sec, PerPeriod: num, PeriodWindow: ""},
+			},
+			`duplicate period ("60s") found`,
+		},
+		{
+			[]*config.RateLimit{
+				{Period: min, PerPeriod: num, PeriodWindow: "test"},
+			},
+			`unsupported 'period_window' ("test") given`,
+		},
+		{
+			[]*config.RateLimit{
+				{Period: min, PerPeriod: num, PeriodWindow: "", Mode: "test"},
+			},
+			`unsupported 'mode' ("test") given`,
+		},
+	} {
+		_, err := ConfigureRateLimits(context.TODO(), tc.configured, nil)
+		if err == nil {
+			t.Fatal("Missing error")
+		}
+
+		if got := err.Error(); got != tc.expMessage {
+			t.Errorf("exp: %q\ngot: %q", tc.expMessage, got)
+		}
+	}
+}
diff --git a/handler/ratelimit/ring_buffer_internal_test.go b/handler/ratelimit/ring_buffer_internal_test.go
new file mode 100644
index 00000000..0e7db64b
--- /dev/null
+++ b/handler/ratelimit/ring_buffer_internal_test.go
@@ -0,0 +1,135 @@
+package ratelimit
+
+import (
+	"testing"
+	"time"
+)
+
+func TestRingBuffer_New(t *testing.T) {
+	r := newRingBuffer(3)
+
+	if len(r.buf) != 3 {
+		t.Errorf("Unexpected r.buf: %#v", r.buf)
+	}
+	if r.len != 3 {
+		t.Errorf("Unexpected r.len: %d", r.len)
+	}
+	if r.r != 0 {
+		t.Errorf("Unexpected r.r: %d", r.r)
+	}
+	if r.w != 2 {
+		t.Errorf("Unexpected r.w: %d", r.w)
+	}
+}
+
+func TestRingBuffer_Put(t *testing.T) {
+	now1 := time.Now()
+	now2 := time.Now()
+	now3 := time.Now()
+	now4 := time.Now()
+
+	r := newRingBuffer(3)
+
+	if !r.buf[0].IsZero() {
+		t.Errorf("Unexpected r.buf[0]: %#v", r.buf[0])
+	}
+	if !r.buf[1].IsZero() {
+		t.Errorf("Unexpected r.buf[1]: %#v", r.buf[1])
+	}
+	if !r.buf[2].IsZero() {
+		t.Errorf("Unexpected r.buf[2]: %#v", r.buf[2])
+	}
+	if got := r.get(); !got.IsZero() {
+		t.Errorf("Unexpected r.get(): %#v", got)
+	}
+
+	r.put(now1)
+
+	if !r.buf[0].Equal(now1) {
+		t.Errorf("Unexpected r.buf[0]: %#v", r.buf[0])
+	}
+	if !r.buf[1].IsZero() {
+		t.Errorf("Unexpected r.buf[1]: %#v", r.buf[1])
+	}
+	if !r.buf[2].IsZero() {
+		t.Errorf("Unexpected r.buf[2]: %#v", r.buf[2])
+	}
+	if got := r.get(); !got.IsZero() {
+		t.Errorf("Unexpected r.get(): %#v", got)
+	}
+
+	if r.r != 1 {
+		t.Errorf("Unexpected r.r: %d", r.r)
+	}
+	if r.w != 0 {
+		t.Errorf("Unexpected r.w: %d", r.w)
+	}
+
+	r.put(now2)
+
+	if !r.buf[0].Equal(now1) {
+		t.Errorf("Unexpected r.buf[0]: %#v", r.buf[0])
+	}
+	if !r.buf[1].Equal(now2) {
+		t.Errorf("Unexpected r.buf[1]: %#v", r.buf[1])
+	}
+	if !r.buf[2].IsZero() {
+		t.Errorf("Unexpected r.buf[2]: %#v", r.buf[2])
+	}
+	if got := r.get(); !got.IsZero() {
+		t.Errorf("Unexpected r.get(): %#v", got)
+	}
+
+	if r.r != 2 {
+		t.Errorf("Unexpected r.r: %d", r.r)
+	}
+	if r.w != 1 {
+		t.Errorf("Unexpected r.w: %d", r.w)
+	}
+
+	r.put(now3)
+
+	if !r.buf[0].Equal(now1) {
+		t.Errorf("Unexpected r.buf[0]: %#v", r.buf[0])
+	}
+	if !r.buf[1].Equal(now2) {
+		t.Errorf("Unexpected r.buf[1]: %#v", r.buf[1])
+	}
+	if !r.buf[2].Equal(now3) {
+		t.Errorf("Unexpected r.buf[2]: %#v", r.buf[2])
+	}
+	if got := r.get(); !got.Equal(now1) {
+		// now1 is the oldest value in the buffer after r.put(now3)
+		t.Errorf("Unexpected r.get(): %#v", got)
+	}
+
+	if r.r != 0 {
+		t.Errorf("Unexpected r.r: %d", r.r)
+	}
+	if r.w != 2 {
+		t.Errorf("Unexpected r.w: %d", r.w)
+	}
+
+	r.put(now4)
+
+	if !r.buf[0].Equal(now4) {
+		t.Errorf("Unexpected r.buf[0]: %#v", r.buf[0])
+	}
+	if !r.buf[1].Equal(now2) {
+		t.Errorf("Unexpected r.buf[1]: %#v", r.buf[1])
+	}
+	if !r.buf[2].Equal(now3) {
+		t.Errorf("Unexpected r.buf[2]: %#v", r.buf[2])
+	}
+	if got := r.get(); !got.Equal(now2) {
+		// now2 is the oldest value in the buffer after r.put(now4)
+		t.Errorf("Unexpected r.get(): %#v", got)
+	}
+
+	if r.r != 1 {
+		t.Errorf("Unexpected r.r: %d", r.r)
+	}
+	if r.w != 0 {
+		t.Errorf("Unexpected r.w: %d", r.w)
+	}
+}
diff --git a/main_test.go b/main_test.go
index 0fc0920b..cb5b483c 100644
--- a/main_test.go
+++ b/main_test.go
@@ -51,6 +51,7 @@ func Test_realmain(t *testing.T) {
 		{"empty string in allowed_methods in endpoint", []string{"couper", "run", "-f", base + "/13_couper.hcl"}, nil, `level=error msg="%s/13_couper.hcl:3,5-27: method contains invalid character(s); " build=dev`, 1},
 		{"invalid method in allowed_methods in endpoint", []string{"couper", "run", "-f", base + "/14_couper.hcl"}, nil, `level=error msg="%s/14_couper.hcl:3,5-35: method contains invalid character(s); " build=dev`, 1},
 		{"invalid method in allowed_methods in api", []string{"couper", "run", "-f", base + "/15_couper.hcl"}, nil, `level=error msg="%s/15_couper.hcl:3,5-35: method contains invalid character(s); " build=dev`, 1},
+		{"rate_limit block in anonymous backend", []string{"couper", "run", "-f", base + "/17_couper.hcl"}, nil, `level=error msg="anonymous backend (\"anonymous_3_11\") cannot define 'beta_rate_limit' block(s)" build=dev`, 1},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(subT *testing.T) {
diff --git a/server/http_test.go b/server/http_test.go
index 44e285e9..61663fbc 100644
--- a/server/http_test.go
+++ b/server/http_test.go
@@ -15,6 +15,7 @@ import (
 	"regexp"
 	"strconv"
 	"strings"
+	"sync"
 	"testing"
 	"text/template"
 	"time"
@@ -26,6 +27,7 @@ import (
 	"github.com/avenga/couper/config/configload"
 	"github.com/avenga/couper/config/runtime"
 	"github.com/avenga/couper/internal/test"
+	"github.com/avenga/couper/logging"
 	"github.com/avenga/couper/server"
 )
 
@@ -732,3 +734,143 @@ func TestHTTPServer_EnvironmentBlocks(t *testing.T) {
 		t.Errorf("Unexpected status code: %d", res.StatusCode)
 	}
 }
+
+func TestHTTPServer_RateLimiterFixed(t *testing.T) {
+	helper := test.New(t)
+	client := newClient()
+
+	shutdown, hook := newCouper("testdata/integration/ratelimit/01_couper.hcl", test.New(t))
+	defer shutdown()
+
+	req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/fixed", nil)
+	helper.Must(err)
+
+	hook.Reset()
+
+	go client.Do(req)
+	time.Sleep(1000 * time.Millisecond)
+	go client.Do(req)
+	time.Sleep(1000 * time.Millisecond)
+	go client.Do(req)
+	time.Sleep(500 * time.Millisecond)
+	go client.Do(req)
+
+	time.Sleep(700 * time.Millisecond)
+
+	entries := hook.AllEntries()
+	if len(entries) != 8 {
+		t.Fatal("Missing log lines")
+	}
+
+	for i, entry := range entries {
+		if entry.Data["type"] != "couper_access" {
+			continue
+		}
+
+		if total := entry.Data["timings"].(logging.Fields)["total"].(float64); total <= 0 {
+			t.Fatal("Something is wrong")
+		} else if i < 4 && total > 500 {
+			t.Errorf("Request %d time has to be smaller as 0.5 seconds, was %fms", i/2, total)
+		} else if i >= 4 && i < 6 && total < 1000 {
+			t.Errorf("Request %d time has to be longer as 1 second, was %fms", i/2, total)
+		} else if i >= 6 && total < 500 {
+			t.Errorf("Request %d time has to be longer as 0.5 seconds, was %fms", i/2, total)
+		}
+	}
+}
+
+func TestHTTPServer_RateLimiterSliding(t *testing.T) {
+	helper := test.New(t)
+	client := newClient()
+
+	shutdown, hook := newCouper("testdata/integration/ratelimit/01_couper.hcl", test.New(t))
+	defer shutdown()
+
+	req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/sliding", nil)
+	helper.Must(err)
+
+	hook.Reset()
+
+	go client.Do(req)
+	time.Sleep(1000 * time.Millisecond)
+	go client.Do(req)
+	time.Sleep(1000 * time.Millisecond)
+	go client.Do(req)
+	time.Sleep(500 * time.Millisecond)
+	go client.Do(req)
+
+	time.Sleep(1700 * time.Millisecond)
+
+	entries := hook.AllEntries()
+	if len(entries) != 8 {
+		t.Fatal("Missing log lines")
+	}
+
+	for i, entry := range entries {
+		if entry.Data["type"] != "couper_access" {
+			continue
+		}
+
+		if total := entry.Data["timings"].(logging.Fields)["total"].(float64); total <= 0 {
+			t.Fatal("Something is wrong")
+		} else if i < 4 && total > 500 {
+			t.Errorf("Request %d time has to be smaller as 0.5 seconds, was %fms", i/2, total)
+		} else if i >= 4 && i < 6 && total < 1000 {
+			t.Errorf("Request %d time has to be longer as 1 second, was %fms", i/2, total)
+		} else if i >= 6 && total < 1500 {
+			t.Errorf("Request %d time has to be longer as 1.5 seconds, was %fms", i/2, total)
+		}
+	}
+}
+
+func TestHTTPServer_RateLimiterBlock(t *testing.T) {
+	helper := test.New(t)
+	client := newClient()
+
+	shutdown, _ := newCouper("testdata/integration/ratelimit/01_couper.hcl", test.New(t))
+	defer shutdown()
+
+	req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/block", nil)
+	helper.Must(err)
+
+	var resps [3]*http.Response
+	var mu sync.Mutex
+
+	go func() {
+		mu.Lock()
+		resps[0], _ = client.Do(req)
+		mu.Unlock()
+	}()
+
+	time.Sleep(400 * time.Millisecond)
+
+	go func() {
+		mu.Lock()
+		resps[1], _ = client.Do(req)
+		mu.Unlock()
+	}()
+
+	time.Sleep(400 * time.Millisecond)
+
+	go func() {
+		mu.Lock()
+		resps[2], _ = client.Do(req)
+		mu.Unlock()
+	}()
+
+	time.Sleep(400 * time.Millisecond)
+
+	mu.Lock()
+
+	if resps[0].StatusCode != 200 {
+		t.Errorf("Exp 200, got: %d", resps[0].StatusCode)
+	}
+	if resps[1].StatusCode != 200 {
+		t.Errorf("Exp 200, got: %d", resps[1].StatusCode)
+	}
+	if resps[2].StatusCode != 429 {
+		t.Errorf("Exp 200, got: %d", resps[2].StatusCode)
+	}
+
+	mu.Unlock()
+}
diff --git a/server/testdata/integration/ratelimit/01_couper.hcl b/server/testdata/integration/ratelimit/01_couper.hcl
new file mode 100644
index 00000000..d9622c8c
--- /dev/null
+++ b/server/testdata/integration/ratelimit/01_couper.hcl
@@ -0,0 +1,51 @@
+server "couper" {
+  endpoint "/fixed" {
+    proxy {
+      backend = "RLFixed"
+    }
+  }
+  endpoint "/sliding" {
+    proxy {
+      backend = "RLSliding"
+    }
+  }
+  endpoint "/block" {
+    proxy {
+      backend = "RLBlock"
+    }
+  }
+}
+
+definitions {
+  backend "RLFixed" {
+    path = "/small"
+    origin = env.COUPER_TEST_BACKEND_ADDR
+
+    beta_rate_limit {
+      period        = "3s"
+      per_period    = 2
+      period_window = "fixed"
+    }
+  }
+  backend "RLSliding" {
+    path = "/small"
+    origin = env.COUPER_TEST_BACKEND_ADDR
+
+    beta_rate_limit {
+      period        = "3s"
+      per_period    = 2
+      period_window = "sliding"
+    }
+  }
+  backend "RLBlock" {
+    path = "/small"
+    origin = env.COUPER_TEST_BACKEND_ADDR
+
+    beta_rate_limit {
+      period        = "1s"
+      per_period    = 2
+      period_window = "fixed"
+	  mode          = "block"
+    }
+  }
+}
diff --git a/server/testdata/settings/17_couper.hcl b/server/testdata/settings/17_couper.hcl
new file mode 100644
index 00000000..deb7933b
--- /dev/null
+++ b/server/testdata/settings/17_couper.hcl
@@ -0,0 +1,12 @@
+server {
+  endpoint "/" {
+    proxy {
+      backend {
+        beta_rate_limit {
+          period = "1m"
+          per_period = 60
+        }
+      }
+    }
+  }
+}