Understanding Route-Level Rate Limiting in ElysiaJS

When building web applications with ElysiaJS and Bun, applying rate limiting to sensitive endpoints—such as user registration or password resets—is crucial to prevent brute-force attacks and spam. However, plugins attached via the .use() method in Elysia apply to all subsequent routes within that instance. If you want to restrict elysia-rate-limit strictly to a single route (e.g., POST /register) without affecting adjacent routes like POST /login, you need to isolate the plugin's scope.

The Problem: Global and Instance-Wide Plugin Application

In Elysia, chaining .use(rateLimit(...)) at the top of a route file applies the rate limiter middleware to every handler defined after it in that instance. If your authRoutes instance defines both /register and /login, attaching rate limiting at the instance level will inadvertently throttle requests to /login as well.

Solution 1: Encapsulate the Route in a Sub-Instance (Recommended)

The cleanest and most idiomatic way to isolate plugins in ElysiaJS is to create an inline sub-instance for the route requiring rate limiting. This keeps your code modular and ensures the middleware only runs where intended.

// auth.routes.ts
import { Elysia } from "elysia";
import { registerRateLimit } from "../plugins/rate-limit";

export const authRoutes = new Elysia()
  // Isolated sub-instance with rate limiting applied only to /register
  .use(
    new Elysia()
      .use(registerRateLimit)
      .post("/register", registerHandler)
  )
  // Unaffected route outside the sub-instance scope
  .post("/login", loginHandler);

In this architecture, registerRateLimit is scoped exclusively to the child Elysia instance containing /register, leaving /login completely unrestricted by that specific rate limit.

Solution 2: Using the Conditional skip Option

If you prefer to keep all routes flat in a single instance, elysia-rate-limit provides a built-in skip option. You can pass a function to evaluate the incoming request URL and skip rate limiting for any endpoint other than /register.

// rate-limit.ts
import { rateLimit } from "elysia-rate-limit";

export const registerRateLimit = rateLimit({
  duration: 900000, // 15 minutes
  max: 1,
  errorResponse: "Too many registration attempts, try again later.",
  skip: (request) => {
    const url = new URL(request.url);
    // Skip rate limiting if the path is NOT /register
    return url.pathname !== "/register";
  }
});

Then simply register the plugin in your main route instance:

// auth.routes.ts
import { Elysia } from "elysia";
import { registerRateLimit } from "../plugins/rate-limit";

export const authRoutes = new Elysia()
  .use(registerRateLimit)
  .post("/register", registerHandler)
  .post("/login", loginHandler);

Which Approach Should You Choose?

  • Sub-Instance Approach: Highly recommended. It leverages Elysia's native context scoping, leading to cleaner code without runtime string checks on every request URL.
  • Conditional skip Approach: Helpful if you want centralized rate limiting rules with path-based filtering or dynamic bypass logic.