/sphinx

Configurable HTTP rate limiter

Primary LanguageGoApache License 2.0Apache-2.0

Sphinx: HTTP Rate Limiting

Sphinx is a rate limiting HTTP proxy, implemented in Go, using leaky buckets.

The name for this project ("Sphinx") comes from the ancient Greek word sphingien, which means "to squeeze" or "to strangle." The Sphinx would stand by the road and stop travelers to ask them a riddle. If they could not answer, she would strangle them. She was often thought of as a guardian and flanked the entrances to temples.

Sphinx

Why?

Rate limiting an API is often required to ensure that clients do not abuse the available resources and that the API is reliably available when multiple clients are requesting data concurrently. Buckets can be created based on various parameters of an incoming request (eg. Authorization, IP address) to configure how requests are grouped for limiting.

Rate limiting functionality is already available in some proxies (eg. Nginx, HAProxy). However, they often use in-memory stores that make rate-limiting when running multiple proxies (e.g. for load balancing) unpredictable. Configuration for these limits also gets complex since it includes many actions such as routing, request/response re-writing, and rate-limiting.

Sphinx is not...

  • Sphinx is not focused on preventing Denial of Service (DoS) attacks or requests from malicious clients. The goal is to expose rate limiting information to clients and enforce balanced use by API clients.

  • Sphinx is not a request forwarding service. Sphinx only allows for very simplistic forwarding to a single host per instance of the rate limiter. Any advanced routing or request handling should be handled by a real proxy (eg. Nginx, HAProxy).

  • Sphinx is not an HTTPS terminator. This keeps the burden of configuring SSL certificates and security outside of Sphinx. Ideally, there is real load balancing and HTTPS termination before a request hits Sphinx.

Rate limit headers and errors

Sphinx will update HTTP response headers for requests that match limits to include details about the rate limit status. Headers are canonicalized, but clients should assume header names are case insensitive.

  • X-RateLimit-Reset: Unix timestamp when the rate limit counter will be reset.
  • X-RateLimit-Limit: The total number of requests allowed in a time period.
  • X-RateLimit-Remaining: Number of requests that can be made until the reset time.
  • X-RateLimit-Bucket: Name of the rate-limit bucket this request belongs to in the configuration.

Limit names can be configured via a configuration file.

Request:

HOST example.com
GET /resource/123
AUTHORIZATION Basic ABCD

Response headers:

Status: 200 OK
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 199
X-RateLimit-Reset: 1394506274
X-RateLimit-Bucket: authorized-users

In case the client hits a rate limit, an empty response with a 429 Too Many Requests status code will be returned.

Request:

HOST example.com
GET /resource/123
AUTHORIZATION Basic ABC

Response headers:

Status: 429 Too Many Requests
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1394506274
X-RateLimit-Bucket: authorized-users

Rate limiting in Sphinx is managed by setting up limits in a yaml configuration file. Details about the configuration format can be found in the annotated example.

It is important to understand the concept of buckets and limits to effectively configure a rate limiter.

Limit: A limit defines a rate limiting policy that Sphinx enforces by counting requests in named buckets. Bucket: A bucket is simply a named value. Each request that matches a limit increments the value of one bucket.

Below is an example of a limit and three requests that increment two bucket values.

Test Limit

match if request path begins with /limited bucket names are defined as name-{ip-address} Allow TWO requests per minute

Setting this limit using the config would look like:

proxy:
  handler: http             # can be {http,httplogger}
  host: http://httpbin.org  # URI for the http(s) backend we are proxying to
  listen: :6634             # bind to host:port. default: height of the Great Sphinx of Giza

storage:
  type: redis       # must be one of {redis, dynamodb, memory}
  host: localhost   # redis hostname. required for redis
  port: 6379        # redis port.     required for redis
  table: table      # table name.     required for dynamodb
  region: us-west-1 # table region.   required for dynamodb

limits:
  test-limit:
    interval: 60  # in seconds
    max: 2        # number of requests allowed in interval
    keys:
      ip: ""      # ip keys require no configuration
    matches:
      paths:
        match_any:
          - "/limited*"

Request One

path: /limited/resource/1 Headers: Host: example.com Authorization: Basic User:Password IP: 10.0.0.1

State: test-limit-10.0.0.1: 1

Request Two

path: /limited/resource/2 Headers: Host: example.com Authorization: Basic Admin:Secure IP: 10.0.0.2

State test-limit-10.0.0.1: 1 test-limit-10.0.0.2: 1

Request Three

path: /limited/resource/3 Headers: Host: example.com Authorization: Basic Admin:Secure IP: 10.0.0.1

State test-limit-10.0.0.1: 2 test-limit-10.0.0.2: 1

The following snippet explains how to define limits in Sphinx:

limit-name:
  interval: 15
  max: 200
  keys:
    headers:
      names:
        - "Authorization"
  matches:
    paths:
      match_any:
        - "/special/resources/.*"

limit_name: Used to identify and added to the X-RateLimit-Bucket header.

interval: A limit may create many buckets. This key provides the expire time in secs for all buckets created for this limit.

max: Maximum number of requests that will be allowed for a bucket in one interval.

keys: This section defines the dynamic bucket name generated for each request. Currently supported matchers include headers and ip. All keys defined are concatenated to create the full bucket name.

headers: Use concatenated header values from requests in the bucket name.

headers:
  encrypt: "SALT_TO_ENCRYPT_VALUE"  # optional
  names:
    - HEADER_NAME_1
    - HEADER_NAME_2

ip: Use the incoming IP Address from the incoming request in the bucket name.

matches: This section defines which requests this limit should be applied to. The request MUST match all of the matchers defined in this block. Currently supported matchers are headers and paths.

headers: This matcher currently supports the match_any key which returns true if any of the list items evaluate to true. eg:

headers:
  match_any:
    - name: "HEADER_NAME"
      match: "REGEX_FOR_MATCHING_HEADER_VALUE"
    - name: "OTHER_HEADER_NAME"  # no match key means just check for existence

paths: This matcher also supports the match_any key.

paths:
  match_any:
    - "/limited/resource/*"
    - "/objects/limited/.*"

Documentation

  • LeakyBucket: LeakyBucket documentation
  • Sphinx: Sphinx documentation

Tests

Sphinx is built and tested against Go 1.15. Ensure this is the version of Go you're running with go version.

Now you can run our test and linting suites via Make:

cd sphinx
make test

Credits

  • Sphinx logo by EricP from The Noun Project
  • Drone inspiration for building a deb