<?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
{
$this->configureRateLimiting();
}
protected function configureRateLimiting(): void
{
RateLimiter::for('api', function (Request $request) {
$user = $request->user();
if ($user) {
$perMinute = $user->onPaidPlan() ? 300 : 60;
return Limit::perMinute($perMinute)
->by('user:' . $user->id)
->response(fn () => $this->tooManyRequests());
}
return Limit::perMinute(20)
->by('ip:' . $request->ip())
->response(fn () => $this->tooManyRequests());
});
RateLimiter::for('uploads', function (Request $request) {
$key = optional($request->user())->id ?: $request->ip();
return Limit::perMinute(5)
->by('upload:' . $key)
->response(fn () => $this->tooManyRequests());
});
}
protected function tooManyRequests()
{
return response()->json([
'message' => 'Rate limit exceeded. Please slow down.',
], 429, ['Retry-After' => 60]);
}
}
<?php
use App\Http\Controllers\Api\ApiController;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
Route::get('/projects', [ApiController::class, 'index']);
Route::get('/projects/{project}', [ApiController::class, 'show']);
Route::middleware('throttle:uploads')->group(function () {
Route::post('/projects/{project}/attachments', [ApiController::class, 'store']);
});
});
// Guest-facing endpoint still throttled per IP by the same 'api' limiter.
Route::middleware('throttle:api')->get('/status', [ApiController::class, 'status']);
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Project;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ApiController extends Controller
{
public function index(Request $request): JsonResponse
{
$projects = $request->user()
->projects()
->latest()
->paginate(25);
return response()->json($projects);
}
public function show(Project $project): JsonResponse
{
$this->authorize('view', $project);
return response()->json($project);
}
public function store(Request $request, Project $project): JsonResponse
{
$this->authorize('update', $project);
$data = $request->validate([
'file' => ['required', 'file', 'max:10240'],
]);
$path = $data['file']->store('attachments');
$attachment = $project->attachments()->create(['path' => $path]);
return response()->json($attachment, 201);
}
public function status(): JsonResponse
{
return response()->json(['ok' => true]);
}
}
Laravel ships a RateLimiter facade that lets applications define named rate-limit policies once and apply them anywhere via the throttle middleware. This snippet shows how to define a per-user policy that falls back to the client IP for guests, expose it as a route middleware alias, and shape the response returned when a caller exceeds the limit.
The AppServiceProvider tab wires everything up in boot(). RateLimiter::for('api') registers a named limiter whose closure receives the current Request and returns a Limit describing how many hits are allowed per window. When the request is authenticated, the limit is keyed by $user->id so every account gets its own bucket; unauthenticated traffic is keyed by $request->ip() so guests are throttled per address. The by() call is what isolates buckets — without it every caller would share one global counter. The provider also registers a second, stricter uploads limiter to show that multiple policies can coexist, and passes a response() callback so exhausted callers receive a clean JSON payload with a Retry-After header instead of Laravel's default plain text.
Because the limiter is a closure, the policy can vary at runtime. In AppServiceProvider the allowed count is raised for users on a paid plan, demonstrating dynamic limits driven by application state rather than a fixed number. Returning Limit::none() would exempt a caller entirely, which is useful for internal service tokens.
The routes/api.php tab attaches the policy with ->middleware('throttle:api'), where api matches the limiter name. The stricter group uses throttle:uploads. This keeps rate-limit configuration out of controllers entirely.
The ApiController tab is deliberately thin — it contains no throttling logic, proving that the concern lives in the limiter definition and middleware. The trade-off of this approach is that limiter state lives in the configured cache store; a single-server file or array store will not coordinate limits across nodes, so production deployments should point the cache at Redis so counters are shared. A common pitfall is keying by a value that changes per request (such as a full URL) which silently defeats the limit, so keys should identify the caller, not the action.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
json.array! @posts do |post|
json.cache! ['v1', post], expires_in: 1.hour do
json.id post.id
json.title post.title
json.excerpt post.excerpt
json.published_at post.published_at
Fragment caching for expensive JSON serialization
Share this code
Here's the card — post it anywhere.