Every request is individually legitimate, and together they do harm. A script that buys every ticket, or creates ten thousand trial accounts.
Here: each license created costs something at the provider. A consumer, compromised or just buggy, that creates licenses in a loop is spending real money through a perfectly valid, authenticated, rate-limited API.
What stops it: the per-minute limit helps but isn’t the right tool, because sixty licenses a minute for a day is still a disaster. The control is a business limit: license_limit on the consumer, checked in store (Chapter 7), in the batch request, and again in each batch job (Chapter 18).
That check reads a count and then acts on it, and two requests arriving together can both read “forty-nine.” Where the limit is a hard promise and not a guardrail, take a lock around the check and the creation:
// app/Http/Controllers/LicenseController.php
public function store(
StoreLicenseRequest $request,
): JsonResponse {
$lock = "license-create:{$request->user()->id}";
$license = Cache::lock($lock, 120)->block(
5,
fn () => $this->createWithinLimit($request),
);
return LicenseResource::make($license)
->response()
->setStatusCode(Response::HTTP_CREATED);
}
private function createWithinLimit(
StoreLicenseRequest $request,
): License {
$consumer = $request->user();
$domain = $request->validated('domain');
$held = Licenses::all();
$license = $this->holding($held, $domain);
if ($license === null) {
if ($held->count() >= $consumer->license_limit) {
throw new LicenseLimitReached;
}
$license = Licenses::create(
name: $request->validated('name'),
domain: $domain,
);
LicenseIssued::dispatch($license, $consumer);
}
return $license;
}
This is store as the book leaves it: Chapter 8’s lookup, Chapter 7’s limit, Chapter 18’s event, and the lock around all three. CreateLicense takes the same lock, under the same name, as Chapter 18 showed. If the lock can’t be had within five seconds, block() throws, and that gets the answer Chapter 8 already defined:
// bootstrap/app.php, inside withExceptions()
$exceptions->render(function (LockTimeoutException $e) {
return response()->json([
'message' => __('errors.request_in_progress'),
'code' => ErrorCode::RequestInProgress,
], Response::HTTP_CONFLICT, ['Retry-After' => 1]);
});
$exceptions->dontReport(LockTimeoutException::class);
The last line keeps a busy moment out of the error tracker. A contended create is a consumer’s 409, not an incident.
One limit of this remains, and Chapter 9 named it: the count comes from a list that a background refresh can briefly put back out of date. Where the limit is a contract and not a guardrail, read the list uncached inside the lock.
How you know: ask of every endpoint that creates something, “what does the thousandth one in an hour cost us, and would anyone notice?” If nobody would notice, add licenses created per consumer per day to the dashboard in Chapter 13, and alert when one leaves its usual range.