Audience: Data Center app and plugin developers, including Marketplace vendors who expose REST endpoints and want to protect them from abusive or runaway traffic.
Available from: the atlassian-ratelimiting 5.0.0 rate-limiting platform plugin, bundled with the following product releases:
| Product | Version |
|---|---|
| Confluence Data Center | 11.0 |
| Jira Software Data Center | 12.0 |
| Jira Service Management Data Center | 12.0 |
| Bitbucket Data Center | 11.0 |
| Bamboo Data Center | 13.0 |
| Crowd Data Center | 8.0 |
The annotation-based, API-specific rate limiter lets you cap the request rate of an individual REST endpoint by adding a single Java annotation to a JAX-RS resource class or method. No filters, no wiring, no product-specific code.
| Property | Behaviour |
|---|---|
| Scope of a limit | Per node and per user. Each user gets their own budget on each cluster node. |
| User identity | Authenticated user; OAuth2 2LO (client credentials) users are limited per client_id; everyone else is limited as a single shared anonymous bucket. |
| Unit | Permits per second (a token bucket with a burst capacity of roughly one second of traffic). |
| When exceeded | The request is rejected with HTTP 429 before it reaches your resource method. |
| Runtime control | Administrators can override each limit with a system property — no code change or rebuild required. For a constant endpoint limit, a value of 0 or lower disables the limit. |
This is not the same as the admin-configured global rate limiting feature (Settings → Rate limiting). That one limits a user's total traffic across the product. This feature protects one specific endpoint. When both a global rate limit and an endpoint-specific rate limit apply, the effective limit is the lower of the two. Endpoint-specific rate limiting is therefore most useful for expensive or abuse-prone APIs, rather than as a replacement for the product-wide global rate limit.
Add the public API artifact with provided scope. It contains only annotations and one interface; the runtime implementation ships with the product.
1 2 3 4 5 6 7<dependency> <groupId>com.atlassian.ratelimiting</groupId> <artifactId>rate-limiting-public-api</artifactId> <version>5.0.0</version> <scope>provided</scope> </dependency>
Put @RateLimited on a JAX-RS resource method (limits that one operation) or on the resource class (applies to every method that does not have its own annotation).
1 2 3 4 5 6 7 8 9 10 11 12import com.atlassian.ratelimiting.annotation.RateLimited; @Path("/questions") public class QuestionsResource { @GET @RateLimited(permitsPerSecond = 15) public Response getQuestions() { // ... } }
The platform discovers the annotation when REST resources start up and installs the enforcement filter for you.
@RateLimited attributes| Attribute | Type | Default | Meaning |
|---|---|---|---|
permitsPerSecond | double | 0 | Allowed requests per second, per user, per node. Fractional values are valid (0.5 = one request every 2 seconds). Any value ≤ 0 means "no limit". |
type | RateLimited.Type | ALL | ALL — limit every user. TWO_LO — limit only OAuth2 2LO (client credentials) requests, keyed by client id; interactive users are untouched. |
propertyName | String | derived | A stable, human-friendly key for the system property that overrides this limit. |
If you do not set propertyName, the override key is derived from the code and is therefore brittle — it changes if you rename the class or the method:
1 2com.atlassian.ratelimiting.api.<lowercased.fully.qualified.classname>.<lowercased methodname>
Whatever you put in propertyName becomes the property name verbatim — the platform does not prepend com.atlassian.ratelimiting.api. to it. Always include the prefix yourself, as in the example below, so your limit stays inside the canonical namespace.
Set an explicit propertyName so administrators get a stable, documentable knob:
1 2 3 4 5@GET @RateLimited(permitsPerSecond = 15, propertyName = "com.atlassian.ratelimiting.api.myapp.questions.list.rps") public Response getQuestions() { /* ... */ }
@RateLimitRuleUse this only when a single constant is not enough — for example when the limit should depend on query parameters, headers, or the authentication type of the request.
Implement Rule. The class must have a public no-arg constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26import com.atlassian.ratelimiting.annotation.RateLimited; import com.atlassian.ratelimiting.annotation.Rule; import jakarta.ws.rs.container.ContainerRequestContext; public class ExpensiveExpandRule implements Rule { // Group name, and the suffix of this rule's system-property override. Keep it stable. @Override public String name() { return "expensive_expand"; } // Should this rule be considered for this request? @Override public boolean appliesTo(RateLimited.Type requestType, ContainerRequestContext ctx) { return requestType == RateLimited.Type.TWO_LO && ctx.getUriInfo().getQueryParameters().containsKey("expand"); } // The limit to apply when this rule applies. @Override public double rateLimit(ContainerRequestContext ctx) { return 5; } }
Reference the rule classes from @RateLimitRule. A @RateLimited annotation must also be present — it supplies the base property name.
1 2 3 4 5 6 7import com.atlassian.ratelimiting.annotation.RateLimitRule; @GET @RateLimited(propertyName = "com.atlassian.ratelimiting.api.myapp.spaces.rps") @RateLimitRule(rules = { ExpensiveExpandRule.class, GenericTwoLoRule.class }) public PageResponse<Space> spaces(...) { /* ... */ }
Rule#name(), which defaults to the simple class name.0 or less means "no limit" for that request.Setting a rule-group property to 0 does not disable that group. A value of 0 or less makes the platform ignore the property and evaluate the rules instead. To effectively disable a group, set a very high permits-per-second value.
Gotchas
@RateLimitRule per class or method. List all rule classes inside its rules() array.@RateLimitRule is present it fully replaces the permitsPerSecond value from @RateLimited — there is no fallback to it.@RateLimitRule without a companion @RateLimited is ignored.appliesTo() and rateLimit() cheap — they run on every request to that endpoint.1 2 3 4 5HTTP/1.1 429 Too Many Requests Retry-After: 1 {"rateLimited": true}
Retry-After is in seconds and is derived from the effective limit: 1 second for rates of 1/s or higher, otherwise the time needed to earn one permit.
All of the following are JVM system properties.
| Property | Purpose | Default |
|---|---|---|
com.atlassian.ratelimiting.api.<lowercased-fqcn>.<lowercased-method> | Override the permits-per-second of one endpoint, using the key derived from the resource class and method. This is checked first. On an endpoint without @RateLimitRule, 0 or a negative value switches the limit off. | annotation value |
<propertyName> | When @RateLimited sets propertyName, that string is the property name exactly as written — the com.atlassian.ratelimiting.api. prefix is not prepended. Include it yourself. | annotation value |
<base>.type | Override the request type (ALL or TWO_LO). <base> is the derived key or your propertyName. | annotation value |
<base>.<ruleName> | Override the limit of one rule group on a rules-based endpoint. <base> is the derived key or your propertyName. | rule value |
com.atlassian.ratelimiting.api.rest.enforce.all | When true, apply a default limit to annotated endpoints that do not declare one. | false |
com.atlassian.ratelimiting.api.rest.rps.default | The default permits-per-second used when rest.enforce.all is true. | 5.0 |
com.atlassian.ratelimiting.api.rest.request.type.default | Default request type when none is specified. | ALL |
com.atlassian.ratelimiting.api.cache.expiry.minutes | Idle expiry of a user's token bucket. | 20 |
propertyName so the limit survives refactoring and can be documented for administrators.type = TWO_LO when the risk comes from integrations rather than from people, so you do not degrade the interactive UI.429 and back off using Retry-After.Rate this page: