Skip to main content

Rate and Quota limit the backend

The Squid Backend SDK offers functionality that enables the backend to rate and quota limit its functions.

This feature allows you to define limits on the functions you've written in your backend.

  • Define limits by scope, such as per user, per IP address, and globally.
    • Specifically, the scope can be: user, ip, or global.
  • Specify quota renewal periods ranging from hourly to annually.
    • Specifically, the renewPeriod can be: hourly, daily, weekly, monthly, quarterly, or annually.
  • You can stack these traits to create complex limits.

Why limit your backend?

Implementing rate and quota limits on your backend functions can help you prevent abuse, protect resources, reduce costs, mitigate Denial-of-Service (DoS) attacks, and ensure quality of service.

The @limits decorator

Squid provides a decorator which you can use to define the limits to apply to a given executable, webhook, or OpenAPI function.

In the following example, a limit of 5 queries per second and 200 queries per month is applied.

Backend code
import { executable, limits, SquidService } from '@squidcloud/backend';

export class ExampleService extends SquidService {
@limits({ rateLimit: 5, quotaLimit: 200 })
@executable()
concat(str1: string, str2: string): string {
return `${str1}${str2}`;
}
}

The @limits decorator takes two optional parameters, rateLimit and quotaLimit.

rateLimit

Can be defined in three forms:

A number

Represents the number of queries per second you want to limit the function to.

@limits({ rateLimit: 5 })

Defaults to global scope.

An object

Enables defining whether the limit is per user, ip, or global.

@limits({ rateLimit: { value: 7, scope: 'user' } })

A list of objects

Allows for stacking multiple limits.

@limits({
rateLimit: [
{ value: 5, scope: 'user' },
{ value: 10, scope: 'ip' }
]
})
Note

All limits in this list are consumed for each query. Even if the first consumed limit signals that it's been exceeded, all other limits will be consumed before the exception is returned to the client. If multiple limits are rejecting the same query, the first one to reject will be the one that is returned to the client.

quotaLimit

Can be defined in the same three forms:

A number

Represents the total number of times the function can be queried.

@limits({ quotaLimit: 5 })

Defaults to global scope and monthly renewal period.

An object

Enables defining the scope and renewal period.

@limits({ quotaLimit: { value: 7, scope: 'user', renewPeriod: 'annually' } })

Note: the scope and renewPeriod are still optional and the global and monthly defaults still apply if not provided.

A list of objects

Allows for stacking multiple limits.

@limits({
quotaLimit: [
{ value: 7, scope: 'user', renewPeriod: 'monthly' },
{ value: 20, scope: 'user', renewPeriod: 'annually' }
]
})
Note

All limits in this list are consumed for each query. Even if the first consumed limit signals that it's been exceeded, all other limits will be consumed before the exception is returned to the client. If multiple limits are rejecting the same query, the first one to reject will be the one that is returned to the client.

Using both rateLimit and quotaLimit

You can use the two parameters together to define both rate and quota limits:

@limits({
rateLimit: [
{ value: 5, scope: 'user' },
{ value: 10, scope: 'ip' }
],
quotaLimit: [
{ value: 7, scope: 'user', renewPeriod: 'monthly' },
{ value: 20, scope: 'user', renewPeriod: 'annually' }
]
})

See the Enforcement section for more information on how these limits are evaluated.

Understanding limits

You can define any number of limits. All limits are consumed for each query. Once any limit is exceeded, it will override all other limits and the query will be rejected.

For example, suppose a function has a monthly quota of 5 queries and an annual quota of 10 queries:

@limits({
quotaLimit: [
{ value: 5, renewPeriod: 'monthly' },
{ value: 10, renewPeriod: 'annually' }
]
})

If 5 queries are made in the first week of a month, then no more queries are allowed for the remainder of the month, even though the annual quota has not been reached. Similarly, if 5 queries per month are made in the first two months of the year, totaling 10 queries, then no more queries are allowed for the remainder of the year, even though the monthly quota has not been reached for remaining months.

Enforcement

Rate limits are always evaluated. Let's explore what this means with two examples. Note: for these examples let us ignore the 3x burst that is permitted by the rate limiter and assume we can only make the chosen number of queries per second.

Hitting the rate limit

Take this example configuration:

@limits({ rateLimit: 5, quotaLimit: 20 })

Let's timeline the budget:

EventRate BudgetQuota BudgetOutcome
Starting values520
Make 5 queries (quickly)015Queries succeed
Make a 6th query (quickly)015Rate limit rejection

The rate limit will reject the 6th query and the quota limit will not be consumed.

Hitting the quota limit

Exceeding the quota, however, will always involve consuming the rate limit.

Take this example configuration:

@limits({ rateLimit: 10, quotaLimit: 5 })

Let's timeline the budget:

EventRate BudgetQuota BudgetOutcome
Starting values105
Make 5 queries (quickly)50Queries succeed
Make a 6th query (quickly)40Quota limit rejection

The quota limit will reject the 6th query but the rate limit will still experience a consumption down to a budget of 4.

When a limit is exceeded

When a limit is surpassed, the function will return an exception with a message "Rate limit on name exceeded" or "Quota on name exceeded" as appropriate.

The name is a string that contains the name of the function, the scope, and (if it is a quota limit) the renew period. If the scope is user or IP, then the user ID or IP address will be included in the string.

User/IP-based limits when the user or IP is not known

When defining user/IP-based limits, if a given client's user or IP is not known for any reason, then they will be bucketed with all other unknown clients to be a single unknown entity. In other words, all users that are not logged in will be considered a single user and consume from the same rate/quota bucket.

For example, with a given limit:

@limits({ rateLimit: { value: 7, scope: 'user' } })

Each logged-in user will have their own bucket of 7 queries per second, while all unknown users will share a single bucket of 7 queries per second.

Atomicity

In cases where queries are submitted as a batch, and the limit is hit partway through the batch of queries, then the entire batch will be rejected. This ensures there are no partial changes being made.

Note

The limits that were consumed by the rejected batch will remain consumed.

Refills & renewals

No carry-over quotas

Unused quota does not carry-over from one period to the next.

Quota renewal time

Each quota has a renewal period defined, and their exact durations are as follows:

PeriodDuration
hourly1 hour
daily1 day
weekly7 days
monthly30 days
quarterly90 days
annually365 days

Squid renews quotas in two ways, whichever comes first:

  1. Periodically: At the top of the hour (the 0th minute of each hour), each quota limit is checked to see if it is eligible for renewal.
  2. On demand: If a query exceeds the quota, but the quota is eligible for renewal at that time, then it will be renewed.

The start time of the quota period is the time that specific quota (unique combination of function, scope, renew period, and value) was first introduced in a backend deployment.

Rate limit refills

The consumption bucket refills gradually and allows bursts of up to 3x the given rate.

Note

Gradual refill as an example: If you define the limit @limits({ rateLimit: 5 }) and a client exceeds the limit, then the client only needs to wait 1/5th of a second (0.2s) before they can make another query.

Changes to the limits

You can change limits at any time by deploying a new backend. For quotas, changes to the limit value for a specific "limit combo" (a unique combination of function, scope, and renewPeriod) will reset the active count. For example, if a user has made 10 calls and the limit is changed from 20 to 15, then the user will be able to make 15 more calls (not 5). If a new backend deployment makes no changes to a given "limit combo", then the active count will not be reset.

Understanding the impacts on your account

When a function call is rejected due to your defined limit being exceeded, it will not count toward your billable usage. However, Squid maintains quotas relevant to your billing plan and will count all of your queries toward your billing plan, regardless of whether they are rejected by your defined limits.

For example, if you define a quota limit:

@limits({ quotaLimit: 5 })

And you make 8 queries, the first 5 will succeed and the last 3 will be rejected. You will only be billed for the 5 successful queries but Squid will have counted 8 queries toward the quota for your account.

For more information on Squid's quotas and billing, view the Quotas and limits documentation.