FilamentPHP Table Lazy Pagination for Large Admin Datasets
Filament tables paginate at the database level, but scale depends on shaping the query with modifyQueryUsing and persistQueryFilters to keep LIMIT/OFFSET and indexed WHERE clauses.
25 Apr 2026, 18:41 UTC

An admin table that works fine with 200 rows becomes unusable at 200,000 rows. The concrete problem is not Filament rendering, it is how the query is built and kept small on every page, filter and search interaction. The useful takeaway is that Filament tables are database-paginated by default, and the engineering lever for scale is shaping the query before it runs with modifyQueryUsing and persistQueryFilters, not adding more frontend tricks.
What Filament does by default
Filament v3 tables use Laravel's paginator under the hood. Pagination is offset-based and runs at the database level. Per-page limits are enforced by simplePaginate, so only the current page rows are hydrated into Livewire.
Searchable columns and filters are translated into SQL WHERE clauses when possible. Livewire debounces the search input, which throttles rapid keystrokes into fewer queries. That combination keeps payload size low, but it only works if the query itself stays lean.
Shaping the query with modifyQueryUsing
modifyQueryUsing is the intended hook to add database-level constraints without breaking Filament's pagination and filter pipeline. It receives the Eloquent Builder before pagination is applied.
A practical pattern is to limit selected columns and apply stable ordering that can use an index.
// app/Filament/Resources/OrderResource.php
public static function table(Table $table): Table
{
return $table
->query(fn () => Order::query()->select(['id','status','total','created_at','customer_id']))
->modifyQueryUsing(function ($query) {
$query->whereNull('deleted_at')
->orderBy('created_at','desc');
})
->columns([
Tables\Columns\TextColumn::make('id'),
Tables\Columns\TextColumn::make('status'),
Tables\Columns\TextColumn::make('total')->money(),
Tables\Columns\TextColumn::make('created_at')->dateTime(),
])
->defaultSort('created_at','desc')
->perPage(25)
->persistFiltersInSession();
}
Run this in the resource class with write access to the app code. The risk is applying ORM-level transformations inside modifyQueryUsing, such as loading relations or using collection methods, which can pull full tables into memory and bypass the intended LIMIT/OFFSET path.
Keeping filter state predictable
Without persistence, filters reset on page refresh. persistQueryFilters or persistFiltersInSession stores filter state in the session so users do not lose context when navigating.
Note the limitation: lazy pagination does not automatically preserve session-based filter state across page refreshes unless persistence is explicitly configured. Upgrading between major Filament versions can alter query builder assumptions, so review modifyQueryUsing callbacks after upgrades.
Verify the query stays lazy
Check that pagination really stays at the database.
- Enable query logging in a local route or Tinker session with read-only DB permissions. Use DB::enableQueryLog() before rendering the table and DB::getQueryLog() after.
- Look for LIMIT and OFFSET in the generated SQL for the main query. The presence of LIMIT indicates database-level pagination.
- Use Laravel Telescope or dd($table->getQuery()) in a debug route to verify that filter and search actions translate to WHERE clauses rather than in-memory filtering.
Expected checks are the presence of LIMIT/OFFSET and WHERE clauses referencing indexed columns. Relevant risks include enabling query logging in production, which adds overhead and may expose data.
Trade-off and limitation
Lazy pagination improves initial load and memory use, but offset pagination degrades on very deep pages. For tables with millions of rows, keyset pagination is more efficient, and Filament does not provide it out of the box. In that case, modifyQueryUsing can be used to add a where key > last_seen pattern, at the cost of losing native page numbers.
Also, pushing filtering logic into SQL is good for performance, but complex filters with multiple joins can still cause slow plans if indexes are missing.
Close by auditing one resource: set perPage, limit selected columns, add modifyQueryUsing for stable ordering, enable persistFiltersInSession, and inspect the generated SQL with DB::getQueryLog(). If you see LIMIT/OFFSET and indexed WHERE clauses, the table will scale with the dataset.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.