Rate Limit HTTP Requests

Problem this snippet solves:

This rule limits HTTP POST requests by user decoded from the Authorization: header. Only $::maxRate requests are allowed within a $::windowSecs -second window for any single user. $::maxRate per user may be specified in a class, or use the default. If no Authorization header exists, 401 Not Auth response is generated. If rate limit is reached, 501 Server Error response is generated.

Rule was tested under load, maxed out @ just under 200 TPS.

Code :

class MaxPOSTRates {
  #userrate
  "DAllen 20"
  "JCaples 10"
  "CWalker 10"
  "AGerace 20"
}

rule RateLimit_HTTPPost {
#
# Deb Allen, F5 Networks
# April 2006
# Tested on LTM v9.2.2 and 9.2.3
#

when RULE_INIT {
  set ::maxRate 10              ;#set later per user from class
  set ::windowSecs 10           ;#global
  #init array if non-existent
  array set ::postHistory { }
  #wipe array if already existent
  array unset ::postHistory
}
when HTTP_REQUEST {
  if { [HTTP::method] eq "POST" } {
    if {[HTTP::header exists Authorization]} {
      #Extract encoded user/pass from header, decode and grab only the user name
      set myUserID [HTTP::username]
      #Look up the per-user maxRate in the table
      set myMaxRate [findclass $myUserID $::MaxPOSTRates " "]
      #If none exists, use the global maxRate set above
      if { $myMaxRate eq "" }{
        set myMaxRate $::maxRate
      }
    } else {
      #If no Auth header, respond "Not Auth"
      HTTP::respond 401
      return
    }
    set currentTime [clock seconds]
    #we need to count requests in last $windowSecs seconds, so mark the cutoff time 
    set windowStart [expr {$currentTime - $::windowSecs}]
    #find POSTs for this userID
    set postCount 0
    #count POSTs within the window, delete those that are older
    foreach { requestID requestTime } [array get ::postHistory ${myUserID}*] {
      #count POSTs with start time > $windowStart, delete the rest
      if { $requestTime > $windowStart } {
        incr postCount 1
      } else {
        unset ::postHistory($requestID)
      }
    }
    if { $postCount < $myMaxRate } {
      #Allow request and add new record to array w/myUserID.rand + currentTime
      set requestID "${myUserID}.[expr { int(10000000 * rand()) }]"
      set ::postHistory($requestID) $currentTime
    } else {
      #Reject request with 501 server error
      log local0. "Service Unavailable - User $myUserID - Current rate: $postCount - Max rate: $myMaxRate"
      HTTP::respond 501
      return
    }
  }
}
}
Published Mar 18, 2015
Version 1.0
  • Would not return a 501, it's not a server fault, it's a client fault and the client is rate limited. Maybe a "429 Too Many Requests" would be better, see https://tools.ietf.org/html/rfc6585