DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Why Queues Don’t Fix Scaling Problems
  • Protecting Go Applications: Limiting the Number of Requests and Memory Consumption
  • Rate Limiting Strategies for Efficient Traffic Management
  • Setting up Request Rate Limiting With NGINX Ingress

Trending

  • How to Build and Optimize AI Models for Real-World Applications
  • Comparing Top Gen AI Frameworks for Java in 2026
  • The ORM Is Over: AI-Written SQL Is the New Data Access Layer
  • Smart Deployment Strategies for Modern Applications

HTTP Throttling Using Lyft Global Ratelimiting

By 
Sudip Sengupta user avatar
Sudip Sengupta
DZone Core CORE ·
Updated Aug. 13, 20 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
8.8K Views

Join the DZone community and get the full member experience.

Join For Free

Sometime ago, for a project of mine, I was looking for a good rate-limiting service. For the scope of that project, the service would run along a front proxy and would rate-limit requests to third-party applications.

Nginx Plus and Kong certainly have rate-limiting features but are not OSS; while I am a bigger fan of OSS. Using Istio service mesh would have been a overkill. Therefore, I decided to use Envoy Proxy + Lyft Ratelimiting.

The aim for this blog is help you get started with the rate-limiting service and configure various combinations of rate-limiting scenarios.

Let’s dive in…

Understanding Lyft Ratelimiter

Ratelimit configuration consists of

  1. Domain: A domain is a container for a set of rate limits. All domains known to the Ratelimit service must be globally unique. They serve as a way for different teams/projects to have rate limit configurations that don’t conflict.
  2. Descriptor: A descriptor is a list of key/value pairs owned by a domain that the Ratelimit service uses to select the correct rate limit. Descriptors are case-sensitive. Examples of descriptors are:
    • (“database”, “users”)
    • (“message_type”, “marketing”),(“to_number”,”2061234567")
    • (“to_cluster”, “service_a”)
    • (“to_cluster”, “service_a”),(“from_cluster”, “service_b”)

Descriptors can also be nested to achieve more complex rate-limiting scenarios.

We will be performing rate limiting based on various HTTP headers. Let’s have a look at the configuration file.

Shell
xxxxxxxxxx
1
50
 
1
domain: apis
2
descriptors:
3
  - key: generic_key
4
    value: global
5
    rate_limit:
6
      unit: second
7
      requests_per_unit: 60
8
  - key: generic_key
9
    value: local
10
    rate_limit:
11
      unit: second
12
      requests_per_unit: 50
13
  - key: header_match
14
    value: "123"
15
    rate_limit:
16
      unit: second
17
      requests_per_unit: 40
18
  - key: header_match
19
    value: "456"
20
    rate_limit:
21
      unit: second
22
      requests_per_unit: 30
23
  - key: header_match
24
    value: post
25
    rate_limit:
26
      unit: second 
27
      requests_per_unit: 20
28
  - key: header_match
29
    value: get
30
    rate_limit:
31
      unit: second 
32
      requests_per_unit: 10
33
  - key: header_match
34
    value: path
35
    rate_limit:
36
      unit: second 
37
      requests_per_unit: 5
38
#Using nested descriptors
39
  - key: custom_header
40
    descriptors:
41
    - key: plan
42
      value: BASIC
43
      rate_limit:
44
        requests_per_unit: 2
45
        unit: second
46
    - key: plan
47
      value: PLUS
48
      rate_limit:
49
        requests_per_unit: 3
50
        unit: second


In the configuration above, it can be clearly seen

  1. There are various different keys with different ratelimit values.
  2. We can use them globally for the entire vhost in envoy or even locally for a particular path.
  3. We can also have nested values of descriptors.

Envoy Front Proxy

Let’s see how we can use them in the envoy config now. Have a look at the configuration below:

Shell
xxxxxxxxxx
1
128
 
1
static_resources:
2
  listeners:
3
  - name: listener_0
4
    address:
5
      socket_address:        
6
        address: 0.0.0.0
7
        port_value: 10000
8
    filter_chains:
9
    - filters:
10
      - name: envoy.http_connection_manager
11
        config:
12
          stat_prefix: ingress_http
13
          codec_type: AUTO
14
          route_config:
15
            name: local_route
16
            virtual_hosts:
17
            - name: nginx
18
              domains:
19
              - "*"
20
              rate_limits:
21
                - stage: 0
22
                  actions:
23
                    - generic_key: 
24
                        descriptor_value: "global"
25
              routes:
26
              - match:
27
                  prefix: "/nginx_1"
28
                route:
29
                  cluster: nginx_1
30
                  include_vh_rate_limits: true
31
                  rate_limits:
32
                    - actions:
33
                        - generic_key:
34
                            descriptor_value: "local"
35
                    - actions:
36
                        - header_value_match:
37
                            descriptor_value: "get"
38
                            headers: 
39
                              - name: ":method"
40
                                prefix_match: "GET" 
41
                    - actions:  #This will be triggered if `X-CustomHeader` is present AND the X-CustomPlan header has a value of either BASIC or PLUS
42
                      - requestHeaders:
43
                          descriptor_key: "custom_header"
44
                          header_name: "X-CustomHeader"
45
                      - requestHeaders:
46
                          descriptor_key: "plan"
47
                          header_name: "X-CustomPlan"                                                                                    
48
              - match:
49
                  prefix: "/nginx_2"
50
                route:
51
                  cluster: nginx_2
52
                  include_vh_rate_limits: true
53
                  rate_limits:  
54
                    - actions:
55
                        - generic_key:
56
                            descriptor_value: "local"
57
                    - actions:
58
                        - header_value_match:
59
                            descriptor_value: "123"
60
                            headers: 
61
                              - name: "X-MyHeader"
62
                                prefix_match: "123"
63
                    - actions:
64
                        - header_value_match:
65
                            descriptor_value: "456"
66
                            headers: 
67
                              - name: "X-MyHeader"
68
                                prefix_match: "456"
69
                    - actions:
70
                        - header_value_match:
71
                            descriptor_value: "post"
72
                            headers: 
73
                              - name: ":method"
74
                                prefix_match: "POST"
75
                    - actions:
76
                        - header_value_match:
77
                            descriptor_value: "path"
78
                            headers: 
79
                              - name: ":path"
80
                                prefix_match: "/nginx"
81
          http_filters:
82
          - name: envoy.rate_limit
83
            config:
84
              domain: apis
85
              failure_mode_deny: false
86
              rate_limit_service:
87
                grpc_service:
88
                  envoy_grpc:
89
                    cluster_name: rate_limit_cluster
90
                  timeout: 0.25s
91
92
          - name: envoy.router
93
94
  clusters:
95
  - name: nginx_1
96
    connect_timeout: 1s
97
    type: strict_dns
98
    lb_policy: round_robin
99
    hosts:
100
    - socket_address:
101
        address: nginx1
102
        port_value: 80
103
104
  - name: nginx_2
105
    connect_timeout: 1s
106
    type: strict_dns
107
    lb_policy: round_robin
108
    hosts:
109
    - socket_address:
110
        address: nginx2
111
        port_value: 80
112
113
  - name: rate_limit_cluster
114
    type: strict_dns
115
    connect_timeout: 0.25s
116
    lb_policy: round_robin
117
    http2_protocol_options: {}
118
    hosts:
119
    - socket_address:
120
        address: ratelimit
121
        port_value: 8081
122
123
admin:
124
  access_log_path: "/dev/null"
125
  address:
126
    socket_address:
127
      address: 0.0.0.0
128
      port_value: 9901


Here's is how it works:

  1. We have defined a single vhost named nginx which matches all domains.
  2. There is global rate-limit defined for this vhost. The descriptor value is global.
  3. Next, we have 2 clusters under this vhost. Namely, nginx1 and nginx2. Routes for path /nginx1 are routed to nginx1 cluster and similarly for nginx2.
  4. For nginx1, there is generic rate limit defined by descriptor value local and then we have rate-limits for different values of standard HTTP headers such as method, path etc., and some custom HTTP headers such as X-CustomHeader.
  5. We have similar rate-limiting set for nginx2 cluster.
  6. These 2 nginx clusters defined here actually refer to 2 different nginx containers running as part of docker-compose stack.

The overall architecture can be visualized as:

Simulation Architecture

Simulation Architecture

All configuration files for this set-up can be found here. Since they are running on the same network as the ratelimiter and envoy proxy, they can be accessed easily using the container name.

In order to run the set-up just clone the repo and do

Shell
xxxxxxxxxx
1
 
1
docker-compose up 


Once the stack is up, you will have

  1. 2 nginx containers running on port 9090 and 9091 of localhost.
  2. Envoy proxy to intercept and relay requests to nginx servers. Envoy admin console can be reached at localhost:9901.
  3. Envoy will be listening as localhost:10000.
  4. Ratelimiting service container with configured rate limits which will be used to envoy.
  5. Redis container which is used by the Ratelimiting service.

It is important to understand that all the applicable actions for a particular path in a cluster are aggregated by the ratelimiter for the result i.e.,

Logical OR of all the applicable limits

Let’s Test Our Setup

Firstly we need to install Vegeta, a load testing framework. It can be done by

Shell
xxxxxxxxxx
1
 
1
brew update && brew install vegeta


Test Scenario 1:
Case: GET request on /nginx_1/ at 100 requests per second
Expected Result: 10% requests successful. ( Logical OR of "descriptor_value": "global" , "descriptor_value": "local" and "descriptor_value": "get" )
Command: echo "GET http://localhost:10000/nginx_1/" | vegeta attack -rate=100 -duration=0 | vegeta report
Actual Result

Shell
xxxxxxxxxx
1
 
1
$ echo "GET http://localhost:10000/nginx_1/" | vegeta attack -rate=100 -duration=0 | vegeta report
2
          Requests      [total, rate, throughput]  1008, 100.12, 10.92
3
          Duration      [total, attack, wait]      10.071526192s, 10.06832056s, 3.205632ms
4
          Latencies     [mean, 50, 95, 99, max]    4.718253ms, 4.514212ms, 7.426103ms, 9.089064ms, 17.916071ms           Bytes In      [total, mean]              68640, 68.10
5
          Bytes Out     [total, mean]              0, 0.00
6
          Success       [ratio]                    10.91%
7
          Status Codes  [code:count]               200:110  429:898
8
          Error Set:                               429 Too Many Requests

— — —

Test Scenario 2:
Case:POST request on /nginx_1/ at 100 requests per second.
ExpectedResult: 50% requests successful. ( Logical OR of "descriptor_value": "global" and "descriptor_value": "local" )
Command: echo "POST http://localhost:10000/nginx_1/" | vegeta attack -rate=100 -duration=0 | vegeta report
ActualResult:

Shell
xxxxxxxxxx
1
 
1
$ echo “POST http://localhost:10000/nginx_1/" | vegeta attack -rate=100 -duration=0 | vegeta report
2
 Requests [total, rate, throughput] 4344, 100.02, 50.56
3
 Duration [total, attack, wait] 43.434227783s, 43.429286664s, 4.941119ms
4
 Latencies [mean, 50, 95, 99, max] 5.190485ms, 5.224978ms, 7.862512ms, 10.340628ms, 20.573212ms
5
 Bytes In [total, mean] 1370304, 315.45
6
 Bytes Out [total, mean] 0, 0.00
7
 Success [ratio]  50.55%
8
 Status Codes [code:count] 200:2196 429:2148
9
 Error Set:                429 Too Many Requests

— — —

Test Scenario 3:
Case: GET request on /nginx_2/ at 100 requests per second with X-MyHeader: 123
Expected Result: 5% requests successful ( Logical OR of "descriptor_value": "global", "descriptor_value": "local", "descriptor_value": "123", and "descriptor_value": "path")
Command: echo "GET http://localhost:10000/nginx_2/" | vegeta attack -rate=100 -duration=0 -header "X-MyHeader: 123" | vegeta report
Actual Result:

Shell
xxxxxxxxxx
1
 
1
$ echo "GET http://localhost:10000/nginx_2/" | vegeta attack -rate=100 -duration=0 -header "X-MyHeader: 123" | vegeta report
2
          Requests      [total, rate, throughput]  3861, 100.03, 5.18
3
          Duration      [total, attack, wait]      38.604406747s, 38.597776398s, 6.630349ms
4
          Latencies     [mean, 50, 95, 99, max]    4.96685ms, 4.673049ms, 7.683458ms, 9.713522ms, 16.875025ms          Bytes In      [total, mean]              124800, 32.32
5
          Bytes Out     [total, mean]              0, 0.00
6
          Success       [ratio]                    5.18%
7
          Status Codes  [code:count]               200:200  429:3661
8
          Error Set:                               429 Too Many Requests

— — —

Test Scenario 4:
Case: POST request on /nginx_2/ at 100 requests per second with X-MyHeader: 456
Expected Result: 5% requests successful ( Logical OR of "descriptor_value": "global", "descriptor_value": "local", "descriptor_value": "post", "descriptor_value": "456", and "descriptor_value": "path")
Command: echo "POST http://localhost:10000/nginx_2/" | vegeta attack -rate=100 -duration=0 -header "X-MyHeader: 456" | vegeta report
Actual Result:

Shell
xxxxxxxxxx
1
 
1
$ echo “POST http://localhost:10000/nginx_2/" | vegeta attack -rate=100 -duration=0 -header “X-MyHeader: 456” | vegeta report
2
 Requests [total, rate, throughput] 2435, 100.04, 5.13
3
 Duration [total, attack, wait] 24.346703709s, 24.339554311s, 7.149398ms
4
 Latencies [mean, 50, 95, 99, max] 5.513994ms, 5.255698ms, 8.239173ms, 10.390515ms, 20.287931ms
5
 Bytes In [total, mean] 78000, 32.03
6
 Bytes Out [total, mean] 0, 0.00
7
 Success [ratio] 5.13%
8
 Status Codes [code:count] 200:125 429:2310
9
 Error Set: 429 Too Many Requests

— — —

Test Scenario 5:
Case: GET request on /nginx_1/ at 100 requests per second with X-CustomHeader: XYZ and X-CustomPlan: PLUS
Expected Result: 3% requests successful ( Logical OR of "descriptor_value": "global", "descriptor_value": "local", "descriptor_value": "get", "descriptor_key": "custom_header", and "descriptor_key": "plan")
Command: echo "GET http://localhost:10000/nginx_1/" | vegeta attack -rate=100 -duration=0 -header "X-CustomHeader: XYZ" -header "X-CustomPlan: PLUS" | vegeta report
Actual Result:

Shell
xxxxxxxxxx
1
 
1
$ echo “GET http://localhost:10000/nginx_1/" | vegeta attack -rate=100 -duration=0 -header “X-CustomHeader: XYZ” -header “X-CustomPlan: PLUS” | vegeta report
2
  Requests [total, rate, throughput] 2372, 100.05, 3.16
3
  Duration [total, attack, wait] 23.71156424s, 23.707710415s, 3.853825ms Latencies [mean, 50, 95, 99, max] 5.396743ms, 5.179951ms, 7.981171ms, 10.084753ms, 15.086778ms
4
  Bytes In [total, mean] 46800, 19.73
5
  Bytes Out [total, mean] 0, 0.00
6
  Success [ratio] 3.16%
7
  Status Codes [code:count] 200:75 429:2297
8
  Error Set: 429 Too Many Requests

— — —

Test Scenario 6:
Case: GET request on /nginx_1/ at 100 requests per second with X-Header: XYZ and X-CustomPlan: PLUS
Expected Result: 10% requests successful ( Logical OR of "descriptor_value": "global", "descriptor_value": "local", "descriptor_value": "get")
Command: echo "GET http://localhost:10000/nginx_1/" | vegeta attack -rate=100 -duration=0 -header "X-Header: XYZ" -header "X-CustomPlan: PLUS" | vegeta report
Actual Result:

Shell
xxxxxxxxxx
1
 
1
$ echo “GET http://localhost:10000/nginx_1/" | vegeta attack -rate=100 -duration=0 -header “X-Header: XYZ” -header “X-CustomPlan: PLUS” | vegeta report
2
 Requests [total, rate, throughput] 1578, 100.07, 10.78
3
 Duration [total, attack, wait] 15.773500478s, 15.769158977s, 4.341501ms
4
 Latencies [mean, 50, 95, 99, max] 4.748516ms, 4.514902ms, 7.076671ms, 8.518779ms, 16.077828ms
5
 Bytes In [total, mean] 106080, 67.22
6
 Bytes Out [total, mean] 0, 0.00
7
 Success [ratio] 10.77%
8
 Status Codes [code:count] 200:170 429:1408
9
 Error Set: 429 Too Many Requests


Scalability

In a production scenario, you might want to run multiple instances of your proxy which can refer to the same ratelimit cluster. The front proxy is basically stateless.

As far as the ratelimiting service is concerned, I would recommend scaling it horizontally and moving out Redis Cache to a cloud-based service like RedisLabs or AWS Elastic-cache.

Also, using separate Redis for Per Second Limits is highly recommended. All you need to do is:

  1. Set the env var, REDIS_PERSECOND: "true"
  2. Set Redis endpoint, REDIS_PERSECOND_URL

Conclusion

We can clearly see that the actual results are pretty close to expected results. We can accomplish all kinds of complex rate limiting scenarios using this and perform request throttling. Feel free to reach out should you have any questions around this.

rate limit Requests

Published at DZone with permission of Sudip Sengupta. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Why Queues Don’t Fix Scaling Problems
  • Protecting Go Applications: Limiting the Number of Requests and Memory Consumption
  • Rate Limiting Strategies for Efficient Traffic Management
  • Setting up Request Rate Limiting With NGINX Ingress

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook