메인 콘텐츠로 건너뛰기

RateLimiter 파사드의 구조

Laravel의 레이트 리밋은 Illuminate\Cache\RateLimiting\Limit 클래스와 RateLimiter 파사드로 구성되어 있습니다. 내부적으로는 캐시 드라이버(기본은 파일 또는 Redis)에 카운터를 보관하고, 요청 수를 추적합니다. throttle 미들웨어가 받은 요청에 대해 RateLimiter::for()에서 정의한 클로저를 실행하고, 제한에 도달했다면 429 Too Many Requests를 반환합니다.

AppServiceProvider에서의 커스텀 리미터 정의

레이트 리밋의 설정은 App\Providers\AppServiceProviderboot() 메서드에서 합니다.
<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        RateLimiter::for('api', function (Request $request) {
            return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
        });
    }
}
RateLimiter::for()의 제1인수는 리미터명으로, throttle 미들웨어에서 참조할 때 사용합니다. 제2인수의 클로저는 Illuminate\Cache\RateLimiting\Limit 인스턴스를 반환해야 합니다.

사용자별·IP 주소별·플랜별 레이트 리밋

인증된 사용자와 게스트로 제한을 다르게 함

RateLimiter::for('uploads', function (Request $request) {
    return $request->user()
        ? Limit::perHour(100)->by($request->user()->id)
        : Limit::perHour(10)->by($request->ip());
});

사용자의 플랜에 따른 제한

RateLimiter::for('api', function (Request $request) {
    $user = $request->user();

    if (! $user) {
        return Limit::perMinute(30)->by($request->ip());
    }

    return match ($user->plan) {
        'enterprise' => Limit::none(),
        'pro'        => Limit::perMinute(500)->by($user->id),
        default      => Limit::perMinute(60)->by($user->id),
    };
});

IP 주소에 의한 글로벌 제한

특정 엔드포인트와 관계없이, IP 주소 단위로 스로틀링합니다.
RateLimiter::for('global', function (Request $request) {
    return Limit::perMinute(1000)->by($request->ip());
});

여러 제한을 조합함

배열로 반환하면, 모든 제한이 평가됩니다. 어느 하나에 도달한 시점에서 429를 반환합니다.
RateLimiter::for('login', function (Request $request) {
    return [
        Limit::perMinute(10)->by($request->ip()),
        Limit::perMinute(5)->by($request->input('email')),
    ];
});
같은 by 값을 갖는 여러 제한을 정의하는 경우에는, 키가 충돌하지 않도록 프리픽스를 붙여 주세요.
RateLimiter::for('uploads', function (Request $request) {
    return [
        Limit::perMinute(10)->by('minute:' . $request->user()->id),
        Limit::perDay(1000)->by('day:' . $request->user()->id),
    ];
});

throttle 미들웨어와 커스텀 리미터명의 지정

throttle 미들웨어에 정의한 리미터명을 전달합니다.
use Illuminate\Support\Facades\Route;

Route::middleware(['throttle:api'])->group(function () {
    Route::get('/user', function () { /* ... */ });
    Route::post('/posts', function () { /* ... */ });
});

bootstrap/app.php에서의 등록

Laravel 11 이후, 미들웨어는 bootstrap/app.php에서 관리합니다.
use Illuminate\Foundation\Application;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__ . '/../routes/web.php',
        api: __DIR__ . '/../routes/api.php',
        apiPrefix: 'api',
    )
    ->withMiddleware(function (\Illuminate\Foundation\Configuration\Middleware $middleware): void {
        $middleware->throttleApi('api');
    })
    ->create();

API 라우트에의 적용 예

1

리미터를 정의한다

AppServiceProvider에 여러 리미터를 정의합니다.
public function boot(): void
{
    // 一般APIアクセス
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });

    // ファイルアップロード
    RateLimiter::for('uploads', function (Request $request) {
        return $request->user()?->isPro()
            ? Limit::perHour(500)->by($request->user()->id)
            : Limit::perHour(50)->by($request->user()?->id ?: $request->ip());
    });

    // ログイン試行
    RateLimiter::for('login', function (Request $request) {
        return [
            Limit::perMinute(10)->by($request->ip()),
            Limit::perMinute(5)->by($request->input('email')),
        ];
    });
}
2

라우트에 미들웨어를 적용한다

// routes/api.php
use Illuminate\Support\Facades\Route;

Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
    Route::get('/user', [\App\Http\Controllers\UserController::class, 'show']);
    Route::get('/posts', [\App\Http\Controllers\PostController::class, 'index']);
});

Route::middleware(['auth:sanctum', 'throttle:uploads'])->group(function () {
    Route::post('/uploads', [\App\Http\Controllers\UploadController::class, 'store']);
});

Route::middleware(['throttle:login'])->group(function () {
    Route::post('/login', [\App\Http\Controllers\AuthController::class, 'login']);
});

응답 헤더(X-RateLimit-*)의 구조

throttle 미들웨어는 제한 정보를 응답 헤더에 자동 부여합니다.
헤더설명
X-RateLimit-Limit허가되어 있는 요청 수
X-RateLimit-Remaining남은 요청 수
Retry-After다음 요청이 가능해질 때까지의 초 수(429 시에만)
X-RateLimit-Reset제한이 리셋되는 UNIX 타임스탬프
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
Retry-After: 45
X-RateLimit-Reset: 1717000000
Content-Type: application/json

{
    "message": "Too Many Requests."
}

커스텀 응답을 반환한다

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)
        ->by($request->user()?->id ?: $request->ip())
        ->response(function (Request $request, array $headers) {
            return response()->json([
                'message' => 'リクエスト制限を超えました。しばらくしてから再試行してください。',
                'retry_after' => $headers['Retry-After'],
            ], 429, $headers);
        });
});

RateLimiter::attempt()를 사용한 수동 체크

throttle 미들웨어를 사용하지 않고, 코드 안에서 임의의 타이밍에 레이트 리밋을 확인하고 싶은 경우에는 RateLimiter::attempt()를 사용합니다.
use Illuminate\Support\Facades\RateLimiter;

class SmsController extends Controller
{
    public function send(Request $request): \Illuminate\Http\JsonResponse
    {
        $key = 'sms:' . $request->user()->id;

        $executed = RateLimiter::attempt(
            key: $key,
            maxAttempts: 5,
            callback: function () use ($request) {
                app(SmsService::class)->send(
                    $request->user()->phone,
                    $request->input('message')
                );
            },
            decaySeconds: 3600,  // 1時間
        );

        if (! $executed) {
            $seconds = RateLimiter::availableIn($key);

            return response()->json([
                'message' => "SMS送信の制限を超えました。{$seconds}秒後に再試行してください。",
            ], 429);
        }

        return response()->json(['message' => 'SMSを送信しました。']);
    }
}

시도 횟수의 확인과 리셋

// 現在の試行回数を取得
$hits = RateLimiter::attempts($key);

// 次のリセットまでの秒数
$seconds = RateLimiter::availableIn($key);

// 制限に達しているか確認
$tooMany = RateLimiter::tooManyAttempts($key, $maxAttempts = 5);

// カウンターを手動でリセット(ログアウト後など)
RateLimiter::clear($key);

로그인 스로틀링의 예

public function login(Request $request): mixed
{
    $key = 'login:' . $request->input('email');

    if (RateLimiter::tooManyAttempts($key, 5)) {
        $seconds = RateLimiter::availableIn($key);

        throw ValidationException::withMessages([
            'email' => "ログイン試行回数が多すぎます。{$seconds}秒後に再試行してください。",
        ]);
    }

    if (! Auth::attempt($request->only('email', 'password'))) {
        RateLimiter::hit($key, 300);  // 5分間カウント

        throw ValidationException::withMessages([
            'email' => 'メールアドレスまたはパスワードが正しくありません。',
        ]);
    }

    RateLimiter::clear($key);

    return redirect()->intended('/dashboard');
}

응답 기반의 레이트 리밋

특정 응답만 카운트하고 싶은 경우에는 after()를 사용합니다. 404 응답만 카운트함으로써 리소스 열거 공격을 방지하는 예:
use Symfony\Component\HttpFoundation\Response;

RateLimiter::for('resource-lookup', function (Request $request) {
    return Limit::perMinute(10)
        ->by($request->user()?->id ?: $request->ip())
        ->after(function (Response $response) {
            return $response->getStatusCode() === 404;
        });
});

Redis를 사용한 레이트 리밋

기본 캐시 드라이버를 Redis로 변경하는 것만으로, throttle 미들웨어도 자동적으로 Redis를 사용합니다.

Redis 드라이버의 설정

// config/cache.php
'default' => env('CACHE_DRIVER', 'redis'),
# .env
CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

throttleWithRedis를 사용한다

Redis 전용의 최적화된 스로틀링 미들웨어를 사용하려면 bootstrap/app.php에서 throttleWithRedis()를 호출합니다.
use Illuminate\Foundation\Application;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__ . '/../routes/web.php',
        api: __DIR__ . '/../routes/api.php',
        apiPrefix: 'api',
    )
    ->withMiddleware(function (\Illuminate\Foundation\Configuration\Middleware $middleware): void {
        $middleware->throttleWithRedis();
    })
    ->create();
이에 의해 throttle 미들웨어가 ThrottleRequestsWithRedis 클래스로 매핑되어, Redis의 아토믹 조작을 사용해 정확한 카운트가 이루어집니다.
throttleWithRedis()를 사용하는 경우에는 반드시 Redis가 이용 가능한 상태로 해 주세요. Redis에의 접속이 실패하면, 요청이 모두 거부될 가능성이 있습니다.

Redis를 사용하는 이점

  • 수평 스케일링 대응 — 여러 서버 인스턴스 간에서 카운터를 공유할 수 있음
  • 고정밀도 — 아토믹 조작으로 레이스 컨디션을 방지
  • TTL 관리 — Redis의 네이티브한 유효 기한 기능으로 카운터를 자동 삭제

관련 페이지

캐시

Redis를 포함한 Laravel의 캐시 드라이버의 설정과 사용법을 확인합니다.
마지막 수정일 2026년 7월 13일