29 June 2026
Laravel Validation: Practical Optimisation Tips for Faster Requests
Learn practical ways to improve Laravel validation performance, especially when working with expensive rule chains, database-backed validation, large request payloads, and wildcard array rules.
Laravel has a powerful validation layer, providing an expressive and generally fast request validation system for your web forms, APIs, imports and webhooks. However, complex rules or large request payloads can still come at a cost.
In this Dev Den article, we're going to go through some of the techniques we use when building validation to ensure we get as much performance out of Laravel's validation system as we can. Some of these changes may not be obvious right away, but they can have a big impact on validation-heavy requests.
1. Using bail on expensive validation chains
Laravel will, by default, run all rules defined within a field's validation chain. This is good for user experience, as users get feedback on everything wrong with their input, rather than getting caught in an edit-and-save cycle where one error is returned each time.
Although this provides a better user experience, it can also be slower and more expensive if a chain includes complex rules or database queries. If the reduced feedback is acceptable, Laravel's bail rule can be used.
'slug' => ['bail', 'required', 'unique:pages,slug'],With this example, if the slug is not provided, the unique rule will not be executed, and the required validation error will be returned for that field. Only if a value has been given will the unique rule then also be run.
2. Ordering matters
Laravel runs validation rules in the order they are defined. This makes it easy to predict how validation will run for a known request payload. With this in mind, we can ensure our rules are optimised so that quick, low-impact rules are first, and heavier rules are last. It is worth noting that if bail is not defined, all rules in the chain will still be run.
'title' => ['bail', 'required', 'string', 'max:150', 'unique:posts,title'],The example rule above will run quick checks on the title first, ensuring it is provided, is a string, and has a maximum length of 150 characters. Only if all these initial rules pass will our database then be queried for the unique rule.
3. Stopping on first failure
If you're validating a large payload, maybe a large API request, import, or webhook, it may be more acceptable to immediately return an error when the first rule fails. This is related to bail, except it applies globally to the validator, so if any field fails validation it immediately stops processing and returns the errors back.
We wouldn't usually suggest this for user-facing forms, as it can impact the experience, but for machine-to-machine communication or large imports it can help improve processing times on invalid data. Do note that this doesn't improve performance for valid data, since all rules on all fields will still need to run.
This can be applied to a form request using the attribute, or manually called on a validator instance.
#[StopOnFirstFailure]
class ImportRequest extends FormRequest
{
public function rules(): array
{
return [
'rows' => ['required', 'array'],
'rows.*.email' => ['bail', 'required', 'email'],
];
}
}Or manually:
$validator = Validator::make($data, $rules)->stopOnFirstFailure();
if ($validator->fails()) {
// error
}4. Be cautious with wildcard rules
Laravel supports using wildcards with nested arrays to provide a convenient way to run validation on a known schema but unknown length. For example, you may have an object containing a list of products for a cart. A typical way to apply rules to this validation would be to use the * wildcard character to apply rules to the nested structures:
'cart' => ['required', 'array'],
'cart.*' => ['array:product_id,quantity'],
'cart.*.product_id' => ['bail', 'required', 'integer', 'exists:products,id'],
'cart.*.quantity' => ['bail', 'required', 'integer', 'min:1'],There are a few potential performance issues with this:
We have no maximum array size defined, meaning our request could submit an unlimited number of items that we'll keep looping over.
For every product, we're doing a database
existscheck. While this is usually okay for a low number of items, bots or large invalid payloads could force a high number of checks, meaning our database could end up being queried thousands of times or more.
To start, we can optimise our initial rules:
'cart' => ['required', 'array', 'max:100'],
'cart.*' => ['array:product_id,quantity'],
'cart.*.product_id' => ['bail', 'required', 'integer'],
'cart.*.quantity' => ['bail', 'required', 'integer', 'min:1'],Now, we've added a maximum number of items, 100 in this case, and we've removed the per-item exists check. Next, we need to re-add this check in a way that runs after the structure of our request has passed, allowing us to batch the database queries.
There are two ways we can do this. In this article, we'll show both using a form request. Depending on the experience you want for your users, you can either use the after hook, or the passedValidation hook.
Using the after hook
public function after(): array
{
return [
function (Validator $validator) {
if ($validator->errors()->isNotEmpty()) {
return;
}
$productIds = collect($validator->getData()['cart'] ?? [])
->pluck('product_id')
->filter()
->unique()
->values();
$foundIds = Product::whereIn('id', $productIds)
->pluck('id');
if ($productIds->diff($foundIds)->isNotEmpty()) {
$validator->errors()->add('cart', 'One or more products are invalid.');
}
},
];
}This combines all of the product existence checks into a single database call. We then check to ensure there's no difference between the submitted product IDs and the IDs found in the database. If there is a difference, we add a validation error for the entire cart.
This works, and is a suitable optimisation that can be added. However, we do lose the validation error for each individual item. In our example, this is not likely to be an issue, but there may be cases where you want to flag the error against each item that was not found. In this instance, you can make use of the passedValidation hook instead.
Using passedValidation for item-level batch rules
This method is usually talked about for merging data into the request payload after validation has completed. However, it can also be used to optimise expensive validation logic and batch it. You also have the added benefit of this expensive logic only being run after all other validation has passed.
Since validation has happened, we can use Laravel's validated method, and we'll have the expanded indexes available as needed. The above after rule could be rewritten as follows:
protected function passedValidation(): void
{
$cartItems = $this->validated('cart', []);
$productIds = collect($cartItems)
->pluck('product_id')
->filter()
->unique()
->values();
$foundIds = Product::whereIn('id', $productIds)
->pluck('id')
->all();
$errors = [];
foreach ($cartItems as $index => $item) {
if (!in_array($item['product_id'], $foundIds, true)) {
$errors["cart.$index.product_id"] = 'The product is not available or is invalid.';
}
}
if (filled($errors)) {
throw ValidationException::withMessages($errors);
}
}This behaves similarly to the after method, but it allows us to return per-item errors, which in some situations can provide users with more useful feedback.
There is actually another approach that is built into Laravel, however, it is not suitable for our cart data object here.
Exists on an array
Laravel's exists rule supports an array of values which should be used in the query. By adding the rule to the array itself, Laravel can perform a whereIn style check to ensure one query per request, rather than one query per item.
Given you have an array of category IDs on a product form, you may initially lean towards a validation rule like this:
'category_ids' => ['required', 'array'],
'category_ids.*' => ['exists:categories,id'],This will cause one exists rule execution for each array item, meaning one query for each one. If we instead move the exists rule to the array itself, Laravel will run an optimised query for the submitted values instead:
'category_ids' => ['required', 'array', 'exists:categories,id'],Each of these methods can help you find the best balance between user experience and performance when completing validation where wildcards, nested arrays, or database-backed rules are needed.
Conclusion
These are some of the things we consider when writing validation in the projects we work on. Laravel validation is already fast for most normal web forms, but performance can become more important when validating large payloads, wildcard rules, imports, API requests, or database-backed rules.
There are many more things to consider, including the overhead of some file and image rules, doing too much work in prepareForValidation, or performing asynchronous calls during validation when they could instead be handled after validation has passed.
Ready to start your project?
Let's chat about your ideas and see how we can bring them to life.
Get in touch