---
title: Rate limiting
summary: null
url: https://www.fastly.com/documentation/guides/concepts/rate-limiting
---


Fastly provides primitives that can be used to apply rate limiting to your service. This is designed to help you control the rate of requests sent to your Fastly services and origin servers from individual clients or clients forming a single identifiable group.

> **WARNING:** The [Edge Rate Limiting](https://docs.fastly.com/products/edge-rate-limiting) product must be enabled on your account by a Fastly employee in order to use the primitives in VCL described on this page. Rate counters are designed to quickly count high volumes of traffic, not to count precisely.

## Use cases for rate limiting

Rate limiting is typically applied for one of two purposes: to prevent abusive use of a website or service (e.g. by a scraping bot or a denial of service attack) or to apply a limit on use of an expensive or billable resource (e.g. to allow up to 1000 requests an hour to an API endpoint).

These use cases have very different rate limiting requirements in practice. Anti-abuse rate limiting usually does not need precision or globally synchronized request counts, but does need to be able to count extremely quickly so as to be able to react to a sudden inrush of requests from a single source. In contrast, resource rate limiting often requires a globally synchronized count and must be precise, but is able to tolerate longer synchronization times and constraints on the maximum rate at which counters can realistically be incremented.

Fastly's rate counter is designed as an anti-abuse mechanism.

> **HINT:** If your service requires *resource rate limiting*, consider using [real-time log streaming](/guides/integrations/non-fastly-services/developer-guide-logging) combined with post-processing within your logging provider. Learn more in a [Fastly customer blog post about this pattern](https://www.integralist.co.uk/posts/rate-limiting/).

## Using rate counters and penalty boxes

Rate counters allow you to count requests by individual client and penalty boxes allow you to penalize clients for exceeding rate limits you set. Accumulated counts are converted to an estimated rate computed over one of three time windows: 1s, 10s or 60s. Rates are always measured in requests per second (RPS).

The window size helps determine the effective time to detection (TTD) as the rate of inbound requests from a given client increases beyond the threshold you have set. A shorter window results in a quicker detection time for attacks at the expense of accuracy. Refer to the [Edge Rate Limiting limitations](/guides/security/rate-limiting/about-rate-limiting/#limitations-and-considerations) to understand these tradeoffs.

Use the `check_rate()` function to determine if a given client has exceeded a specified request rate. In VCL, the function can be used in any part of the VCL state machine and therefore can be applied to all inbound requests (when placed inside the `vcl_recv` subroutine) or only HTTP traffic to the origin (when placed inside `vcl_miss` or `vcl_pass` subroutines).

> **HINT:** Compute customers can find equivalent functions in [Compute SDKs](/reference/compute/sdks/). The behavior of the Compute functions matches the behavior described here for VCL.

The following example will check if the user (identified by IP address) has exceeded 100 requests per second over the last 10 seconds and, if so, penalize them for 15 minutes:

<!-- TabbedPanels component: 
<Panel id="vcl" full>

```vcl
penaltybox pb { }
ratecounter rc { }

sub vcl_recv {
  if (fastly.ff.visits_this_service == 0 && ratelimit.check_rate(client.ip, rc, 1, 10, 100, pb, 15m)) {
    error 429 "Too many requests";
  }
}
```

</Panel>
<Panel id="rust" full>

```rust
use std::time::Duration;

use fastly::{
    erl::{Penaltybox, RateCounter, RateWindow, ERL},
    http::StatusCode,
    Error, Request, Response,
};

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    // Open the rate counter and penalty box.
    let rc = RateCounter::open("rc");
    let pb = Penaltybox::open("pb");

    // Open the Edge Rate Limiter based on the rate counter and penalty box.
    let limiter = ERL::open(rc, pb);

    // Check if the request should be blocked and update the rate counter.
    let result = limiter.check_rate(
        &req.get_client_ip_addr().unwrap().to_string(), // The client to rate limit.
        1,                            // The number of requests this execution counts as.
        RateWindow::SixtySecs,        // The time window to count requests within.
        100, // The maximum average number of requests per second calculated over the rate window.
        Duration::from_secs(15 * 60), // The duration to block the client if the rate limit is exceeded.
    );

    let is_blocked: bool = match result {
        Ok(is_blocked) => is_blocked,
        Err(err) => {
            // Failed to check the rate. This is unlikely but it's up to you if you'd like to fail open or closed.
            eprintln!("Failed to check the rate: {:?}", err);
            false
        }
    };

    if is_blocked {
        return Ok(Response::from_status(StatusCode::TOO_MANY_REQUESTS)
            .with_body_text_plain("You have sent too many requests recently. Try again later."));
    }

    let beresp = req.send("protected_backend")?;
    Ok(beresp)
}
```

</Panel>
<Panel id="go" full>

```go
package main

import (
	"context"
	"fmt"
	"io"
	"os"
	"time"

	"github.com/fastly/compute-sdk-go/erl"
	"github.com/fastly/compute-sdk-go/fsthttp"
)

func main() {
	fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) {
		rc := erl.OpenRateCounter("rc")
		pb := erl.OpenPenaltyBox("pb")

		limiter := erl.NewRateLimiter(rc, pb)

		policy := &erl.Policy{
			RateWindow:         erl.RateWindow10s,      // The time window to count requests within.
			MaxRate:            100,                    // The maximum average number of requests per second calculated over the rate window.
			PenaltyBoxDuration: time.Minute * 15,       // The duration to block the client if the rate limit is exceeded.
		}

		block, err := limiter.CheckRate(
			r.RemoteAddr,  // The client to rate limit.
			1,             // The number of requests this execution counts as.
			policy,        // The rate limiting policy to enforce.
		)

		if err != nil {
			// Failed to check the rate. This is unlikely but it's up to you if you'd like to fail open or closed.
			fmt.Fprintln(os.Stderr, "Failed to check the rate: ", err)
		}

		if block {
			w.WriteHeader(fsthttp.StatusTooManyRequests)
			fmt.Fprintln(w, "You have sent too many requests recently. Try again later.")
			return
		}

		resp, err := r.Send(ctx, "protected_backend")

		if err != nil {
			w.WriteHeader(fsthttp.StatusBadGateway)
			fmt.Fprintln(w, err)
			return
		}

		w.Header().Reset(resp.Header)
		w.WriteHeader(resp.StatusCode)
		io.Copy(w, resp.Body)
	})
}
```

</Panel>
 -->

## Using two count periods (VCL only)

It can sometimes be necessary to apply both a "sustained rate" limit and a "burst rate" limit. This is a common enough use case that the dedicated VCL function `ratelimit.check_rates()` is available to increment two [`ratecounter`](/reference/vcl/declarations/ratecounter) instances in one call.

The example above can be modified to use two counters - here we apply one limit over a long period ("sustained rate") and a higher limit over a shorter period ("burst rate"):

<Fiddle id="6a4424a1" />

## Low level interfaces

Sometimes you may want to interact with a rate counter or penalty box explicitly. For this purpose, the `ratelimit.ratecounter_increment`, `ratelimit.penaltybox_add`, and `ratelimit.penaltybox_has` functions are available in VCL, and their equivalents are available on the rate counter and penalty box types in the Compute SDKs, respectively.

The time domain in count periods is important. If a rate of 100rps is selected with a 60 second window, that will equally allow 100rps for sixty seconds or 6000rps for one second.

For the best performance, try to implement your desired configuration with as few calls to rate limiting primitives as possible.

> **HINT:** Defining rate limiting configuration elements such as RPS limits in a dedicated location such as a [dictionary](/guides/full-site-delivery/dictionaries/about-dictionaries) or [config store](/guides/compute/edge-data-storage/working-with-config-stores) may provide a simpler experience if these values need to be changed.

## Reporting variables

The following variables report an estimate of the bucket count (total value of increments) and rate (amount by which the value is increasing each second) for a given entry. In VCL, this is the most recently incremented entry in the rate counter. They are populated when you call the `ratelimit.check_rate()`, `ratelimit.check_rates()` or `ratelimit.ratecounter_increment()` functions.

For example, if you call `ratelimit.check_rates()` using `client.ip` as the *entry* parameter, and then read the value of a `ratecounter.*` variable, the value returned will be specific to the client IP that is making the current request.

In Compute, rather than reading variables containing data for the latest entry, the rate counter and penalty box types provide lookup functions that take the entry's key as a parameter.

### Estimated bucket counts

These can be used to obtain more detailed estimated counts collected by a rate counter. The counts are divided into 10 second buckets over the last minute. Each bucket represents the estimated number of requests received up to and including that 10 second window of time across the entire Fastly POP.

<!-- TabbedPanels component: 
<Panel id="vcl">

* `ratecounter.{NAME}.bucket.10s`
* `ratecounter.{NAME}.bucket.20s`
* `ratecounter.{NAME}.bucket.30s`
* `ratecounter.{NAME}.bucket.40s`
* `ratecounter.{NAME}.bucket.50s`
* `ratecounter.{NAME}.bucket.60s`

These are `INTEGER` variables.

</Panel>
<Panel id="rust" full>

```rust compile_fail
let count = rc.lookup_count(&entry, fastly::erl::CounterDuration::TenSec);
```

</Panel>
<Panel id="go" full>

```go
count, err := rc.LookupCount(entry, erl.CounterDuration10s)
```

</Panel>
 -->

Buckets are not continuous. For example, if the current time is 12:01:03, then the `10s` bucket represents increments received between 12:01:00 and 12:01:10, not between 12:00:53 and 12:01:03. This means that, in each minute at the ten second mark (e.g., :00, :10, :20) the window represented by each bucket will shift to the next interval.

Estimated bucket counts are not precise and should not be used as counters.

### Estimated rates

In addition to estimated counts, estimated _rates_ are also provided.

<!-- TabbedPanels component: 
<Panel id="vcl">

* `ratecounter.{NAME}.rate.1s`
* `ratecounter.{NAME}.rate.10s`
* `ratecounter.{NAME}.rate.60s`

Each of these variables use `FLOAT` precision.

</Panel>
<Panel id="rust" full>

```rust compile_fail
let rate = rc.lookup_rate(&entry, RateWindow::OneSec);
```

</Panel>
<Panel id="go" full>

```go
rate, err := rc.LookupRate(entry, erl.RateWindow1s)
```

</Panel>
 -->

These variables provide an estimate of the number of increments per second collected by the counter for the entry, calculated over the specified windows of time.

## Alternative keys for clients

Each entry in a rate counter or penalty box has a key. The most common key is an IP address but any string value can be provided. In the following example, a string variable is created and populated with the chosen identifier for the client, and then that variable can be used in the call to `check_rate()`:

<!-- TabbedPanels component: 
<Panel id="vcl" full>

```vcl
penaltybox pb { }
ratecounter rc { }

sub vcl_recv {
  declare local var.entry STRING;
  set var.entry = req.http.Fastly-Client-IP; # <-- Set a custom identifier for the client
  if (fastly.ff.visits_this_service == 0 && ratelimit.check_rate(var.entry, rc, 1, 10, 100, pb, 10m)) {
    error 429 "Too many requests";
  }
}
```

</Panel>
<Panel id="rust" full>

```rust compile_fail
// Set a custom identifier for the client
let entry = req.get_path();

let result: Result<bool, fastly::erl::ERLError> = limiter.check_rate(
    entry, // Use the custom identifier
    1,
    RateWindow::SixtySecs,
    100,
    Duration::from_secs(15 * 60),
);
```

</Panel>
<Panel id="go" full>

```go
// Set a custom identifier for the client.
entry := r.URL.Path

block, err := limiter.CheckRate(
  entry,  // Use the custom identifier
  1,
  policy,
)
```

</Panel>
 -->

Common patterns for keying rate counters include:

**IP address**

<!-- TabbedPanels component: 
<Panel id="vcl" full>

```vcl
set var.entry = req.http.Fastly-Client-IP;
```

</Panel>
<Panel id="rust" full>

```rust compile_fail
let entry = req.get_client_ip_addr().unwrap().to_string();
```

</Panel>
<Panel id="go" full>

```go
entry := r.RemoteAddr
```

</Panel>
 -->

**IP address and User Agent**

<!-- TabbedPanels component: 
<Panel id="vcl" full>

```vcl
set var.entry = req.http.Fastly-Client-IP + req.http.User-Agent;
```

</Panel>
<Panel id="rust" full>

```rust compile_fail
let entry = format!("{}{}", req.get_client_ip_addr().unwrap(), req.get_header(fastly::http::header::USER_AGENT).map(|h| h.to_str().expect("invalid User-Agent")).unwrap_or_default());
```

</Panel>
<Panel id="go" full>

```go
entry := r.RemoteAddr + r.Header.Get("User-Agent")
```

</Panel>
 -->

**IP address and a custom HTTP header**

<!-- TabbedPanels component: 
<Panel id="vcl" full>

```vcl
set var.entry = req.http.Fastly-Client-IP + req.http.Custom-Header;
```

</Panel>
<Panel id="rust" full>

```rust compile_fail
let entry = format!("{}{}", req.get_client_ip_addr().unwrap(), req.get_header("Custom-Header").map(|h| h.to_str().expect("invalid Custom-Header")).unwrap_or_default());
```

</Panel>
<Panel id="go" full>

```go
entry := r.RemoteAddr + r.Header.Get("Custom-Header")
```

</Panel>
 -->

**IP address and URL path**

<!-- TabbedPanels component: 
<Panel id="vcl" full>

```vcl
set var.entry = req.http.Fastly-Client-IP + req.url.path;
```

</Panel>
<Panel id="rust" full>

```rust compile_fail
let entry = format!("{}{}", req.get_client_ip_addr().unwrap(), req.get_url().path());
```

</Panel>
<Panel id="go" full>

```go
entry := r.RemoteAddr + r.URL.Path
```

</Panel>
 -->
