Rate limit based on the number of success request

I am using the below configuration,

stick-table type binary size 100k expire 5s store http_req_rate(5s)
acl abuse sc_http_req_rate(0) ge 5
http-request track-sc0 hdr(room) unless abuse
http-request deny if { sc_http_req_rate(0) gt 5 }

My intention is to count only the success request, so i have added unless criteria to track-sc0.

But for some reason its not working. It appears counter is incrementing even for denied request also.

Any pointer to correct the conditions would be of great help

Here’s what I can up with. Just tracking the responses looking for a 200 OK and if so then count it which is what I’d consider a success. I had to call the key and table directly or it wouldn’t ever trigger properly.

frontend fe_main from unnamed_defaults_1
  mode http
  bind *:80
  log global
  option httplog
  http-request deny deny_status 404 if { src,table_http_req_rate(fe_main) gt 5 }
  default_backend be_app
  stick-table type ip size 100k expire 30s store http_req_rate(5s)
  http-response track-sc0 src table fe_main if { status 200 }

Thank you for your comment. But could you please help me to understand what was not correct in the configuration which i have shared… basically I would be feeding this frontend config to haproxy ingress controller. Whatever you have provided that also apparently not worked for me.

global
  daemon
  localpeer local
  master-worker
  pidfile /var/run/haproxy.pid
  stats socket /var/run/haproxy-runtime-api.sock expose-fd listeners level admin
  stats timeout 36000
  tune.ssl.default-dh-param 2048
  ssl-default-bind-options no-sslv3 no-tls-tickets no-tlsv10
  ssl-default-bind-ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK
  hard-stop-after 1800000
  log 127.0.0.1 local0 notice
  server-state-file global
  server-state-base /var/state/haproxy/
  default-path config

defaults
  log global
  log-format '%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs "%HM %[var(txn.base)] %HV"'
  option redispatch 0
  option dontlognull
  option http-keep-alive
  timeout http-request 5000
  timeout connect 5000
  timeout client 50000
  timeout queue 5000
  timeout server 50000
  timeout tunnel 3600000
  timeout http-keep-alive 60000
  load-server-state-from-file global

peers localinstance
  peer local 127.0.0.1:10000

frontend healthz
  mode http
  bind 0.0.0.0:1042 name v4
  bind :::1042 name v6 v4v6
  monitor-uri /healthz
  option dontlog-normal

frontend http
  mode http
  bind 0.0.0.0:8080 name v4
  bind :::8080 name v6
  http-request set-var(txn.base) base
  http-request set-var(txn.path) path
  http-request set-var(txn.host) req.hdr(Host),field(1,:),lower
  http-request set-var(txn.host_match) var(txn.host),map(/etc/haproxy/maps/host.map)
  http-request set-var(txn.host_match) var(txn.host),regsub(^[^.]*,,),map(/etc/haproxy/maps/host.map,'') if !{ var(txn.host_match) -m found }
  http-request set-var(txn.path_match) var(txn.host_match),concat(,txn.path,),map(/etc/haproxy/maps/path-exact.map)
  http-request set-var(txn.path_match) var(txn.host_match),concat(,txn.path,),map_beg(/etc/haproxy/maps/path-prefix.map) if !{ var(txn.path_match) -m found }
  ###_config-snippet_### BEGIN
  http-request deny deny_status 404 if { sc_http_req_rate(0) gt 5 }
  stick-table type string size 100k expire 30s store http_req_rate(5s)
  default_backend default_echoserver_8080
  http-response track-sc0 hdr(room) if { status 200 }
  ###_config-snippet_### END
  use_backend %[var(txn.path_match),field(1,.)]
  default_backend default_local_backend

frontend https
  mode http
  bind 0.0.0.0:8443 name v4 crt /etc/haproxy/certs/frontend ssl alpn h2,http/1.1
  bind :::8443 name v6 crt /etc/haproxy/certs/frontend ssl alpn h2,http/1.1
  http-request set-var(txn.base) base
  http-request set-var(txn.path) path
  http-request set-var(txn.host) req.hdr(Host),field(1,:),lower
  http-request set-var(txn.host_match) var(txn.host),map(/etc/haproxy/maps/host.map)
  http-request set-var(txn.host_match) var(txn.host),regsub(^[^.]*,,),map(/etc/haproxy/maps/host.map,'') if !{ var(txn.host_match) -m found }
  http-request set-var(txn.path_match) var(txn.host_match),concat(,txn.path,),map(/etc/haproxy/maps/path-exact.map)
  http-request set-var(txn.path_match) var(txn.host_match),concat(,txn.path,),map_beg(/etc/haproxy/maps/path-prefix.map) if !{ var(txn.path_match) -m found }
  http-request set-header X-Forwarded-Proto https
  ###_config-snippet_### BEGIN
  http-request deny deny_status 404 if { sc_http_req_rate(0) gt 5 }
  stick-table type string size 100k expire 30s store http_req_rate(5s)
  default_backend default_echoserver_8080
  http-response track-sc0 hdr(room) if { status 200 }
  ###_config-snippet_### END
  use_backend %[var(txn.path_match),field(1,.)]
  default_backend default_local_backend

frontend stats
  mode http
  bind *:1024 name stats
  bind :::1024 name v6
  stats enable
  stats uri /
  stats refresh 10s
  http-request set-var(txn.base) base
  http-request use-service prometheus-exporter if { path /metrics }

backend default_echoserver_8080
  mode http
  balance roundrobin
  option forwardfor
  no option abortonclose
  default-server check
  server SRV_1 10.211.0.11:8080 enabled
  server SRV_2 127.0.0.1:8080 disabled
  server SRV_3 127.0.0.1:8080 disabled
  server SRV_4 127.0.0.1:8080 disabled
  server SRV_5 127.0.0.1:8080 disabled
  server SRV_6 127.0.0.1:8080 disabled
  server SRV_7 127.0.0.1:8080 disabled
  server SRV_8 127.0.0.1:8080 disabled
  server SRV_9 127.0.0.1:8080 disabled
  server SRV_10 127.0.0.1:8080 disabled
  server SRV_11 127.0.0.1:8080 disabled
  server SRV_12 127.0.0.1:8080 disabled
  server SRV_13 127.0.0.1:8080 disabled
  server SRV_14 127.0.0.1:8080 disabled
  server SRV_15 127.0.0.1:8080 disabled
  server SRV_16 127.0.0.1:8080 disabled
  server SRV_17 127.0.0.1:8080 disabled
  server SRV_18 127.0.0.1:8080 disabled
  server SRV_19 127.0.0.1:8080 disabled
  server SRV_20 127.0.0.1:8080 disabled
  server SRV_21 127.0.0.1:8080 disabled
  server SRV_22 127.0.0.1:8080 disabled
  server SRV_23 127.0.0.1:8080 disabled
  server SRV_24 127.0.0.1:8080 disabled
  server SRV_25 127.0.0.1:8080 disabled
  server SRV_26 127.0.0.1:8080 disabled
  server SRV_27 127.0.0.1:8080 disabled
  server SRV_28 127.0.0.1:8080 disabled
  server SRV_29 127.0.0.1:8080 disabled
  server SRV_30 127.0.0.1:8080 disabled
  server SRV_31 127.0.0.1:8080 disabled
  server SRV_32 127.0.0.1:8080 disabled
  server SRV_33 127.0.0.1:8080 disabled
  server SRV_34 127.0.0.1:8080 disabled
  server SRV_35 127.0.0.1:8080 disabled
  server SRV_36 127.0.0.1:8080 disabled
  server SRV_37 127.0.0.1:8080 disabled
  server SRV_38 127.0.0.1:8080 disabled
  server SRV_39 127.0.0.1:8080 disabled
  server SRV_40 127.0.0.1:8080 disabled
  server SRV_41 127.0.0.1:8080 disabled
  server SRV_42 127.0.0.1:8080 disabled

backend default_local_backend
  mode http
  balance roundrobin
  option forwardfor
  no option abortonclose
  default-server check
  server ingress_controller 127.0.0.1:6061

My HaProxy version “HAProxy version 2.6.9-3a3700a 2023/02/14”

Your setup here says to track hdr(room) so if it’s not working did your requests have a header called “room”? If they didn’t have that header it wouldn’t be able to track anything. If you include the header then it works.

When you implemented the suggestion I gave you also used hdr(room) and if it was also failing then that implies your incoming requests don’t contain that header. I tested both configurations on a lab device and once I included that header then it limited it properly.

My request header had the key and corresponding value for “room”. Basically instead of IP i wanted my own key.

When i use the below configuration it works properly…

stick-table type binary size 100k expire 5s store http_req_rate(5s)
http-request track-sc0 hdr(room) 
http-request deny if { sc_http_req_rate(0) gt 5 }

problem with this is counter would be incremented for the failed attempt also. Because of which if the request is coming at constant pace which violates the rate-limt then only first 5 request would be entertained and remaining all the request would be denied. That is why i wanted to increment the counter if the rate-limit is not exceeded and if rate limit is exceeded will not increment the counter unless the existing one gets expired.

Ok I think I understand now what you’re getting at. You need it to bleed off connections if it’s been long enough and you want to use a custom key. I will say that much of what you describe can be done with setting connection limits on the servers but for the sake of discussion give this a try because it allows for a custom key.

  http-request deny deny_status 404 if { int("200"),table_http_req_rate(fe_main) gt 5 }
  stick-table type integer size 100k expire 5s store http_req_rate(5s)
  http-response track-sc0 status if { status 200 }

I used status code on response because if I used hdr(room) it wouldn’t work for me. My server doesn’t return that header. Therefore I am looking at the responses from the server side and saying if it’s 200 OK then track it and it only stores that for a set amount of time. Otherwise it does a 404.

IF your server is returning your custom header you could try this:

  http-request deny deny_status 404 if { str("room"),table_http_req_rate(fe_main) gt 5 }
  stick-table type integer size 100k expire 5s store http_req_rate(5s)
  http-response track-sc0 hdr(room) if { status 200 }

My philosophy is that because it’s blocking on the front but observing the back then once the block kicks in it will allow the server side (backend) to age out. In testing this appears to work for me.

$ ab -H "room: 5" -n500000 -c10 http://12.1.1.4/
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
...snip...
Finished 500000 requests


Time taken for tests:   38.611 seconds
Complete requests:      500000
Failed requests:        499919
   (Connect: 0, Receive: 0, Length: 499919, Exceptions: 0)
Non-2xx responses:      499919 <-------

So over the span of 38.6 seconds it allowed 81 connections which should be pretty dang close.