DataTables: Choosing Client-Side vs Server-Side Processing for Large Datasets
A decision guide for DataTables pagination: when client-side processing stops scaling, what serverSide:true really requires, and how to verify your endpoint honors the contract.
25 Jul 2025, 19:27 UTC

Your table renders fine with 500 rows. At 50,000 rows the page takes 12 seconds to load and the browser tab starts swapping. The decision you face is not whether to paginate — DataTables always paginates — but where the pagination, sorting, and searching happen: in the browser with the full dataset in memory, or on the server with one page transferred per request.
The short version: client-side processing is correct up to roughly tens of thousands of rows; past that, switch to serverSide: true and implement DataTables' request/response contract on your backend. The rest of this guide compares the options and shows what a correct server-side endpoint looks like.
The decision and its constraints
Client-side mode means your page (or an initial Ajax call) delivers every row once. DataTables then handles paging, sorting, and global search entirely in JavaScript. Server-side mode means every draw — page change, sort, search keystroke — triggers an Ajax request, and your backend returns only the rows for that page plus two counts.
The constraints that usually decide it:
- Row count and row weight. Transfer size and browser memory grow linearly with rows. Wide rows with many columns hit the wall sooner.
- Backend capacity. Server-side mode requires an endpoint that understands DataTables' parameters. If you cannot touch the backend, client-side is your only option.
- Computed or per-column rendering. Anything the client renders must exist in the data it receives. Under server-side mode the client only ever sees one page, so computed fields must be produced server-side.
- Search semantics. Client-side search is DataTables' built-in behavior. Server-side search is whatever your SQL (or equivalent) does — case sensitivity and partial matching become your responsibility.
Comparing the supported options
| Option | Practical scale | Sort/search UX | Backend work | Main risk |
|---|---|---|---|---|
| Client-side (default) | Up to ~tens of thousands of rows | Instant; all operations in-browser | None beyond serving the data | Initial load time and memory blow up with dataset size |
Server-side (serverSide: true) | Millions of rows | One network round trip per draw | Implement paging, ordering, search, and counts | Wrong recordsFiltered breaks pagination totals |
| Scroller extension (virtual scrolling) | Very large datasets | Continuous scroll instead of page buttons | Same server-side contract for large sources | UX change; not a drop-in replacement for discrete pages |
Trade-offs that bite in practice
The silent failure mode. Setting serverSide: true while your endpoint still returns the full dataset is a common mistake. The table works, pagination appears correct, and you have gained nothing — the transfer cost is identical to client-side mode. Always verify the response payload size.
The count contract. Your endpoint must return two counts: recordsTotal (rows in the table with no search applied) and recordsFiltered (rows matching the current search). If your page query applies a filter but your recordsFiltered query does not — or vice versa — DataTables shows the wrong number of pages and "Showing X of Y" lies to the user.
Deep offsets. Server-side paging typically maps start/length to SQL OFFSET/LIMIT. Offset pagination degrades at large offsets because the database must scan and discard the skipped rows. Keyset (cursor) pagination avoids this but is hard to combine with DataTables' arbitrary per-column ordering. For most admin-style tables, offset paging is acceptable; measure before optimizing.
Concrete implementation sketch
Front end (DataTables 1.x/2.x jQuery-style initialization; parameter names are stable across these versions, but confirm against the version you run):
new DataTable('#orders', {
serverSide: true,
processing: true,
ajax: '/api/orders',
columns: [
{ data: 'id' },
{ data: 'customer' },
{ data: 'total' }
]
});
On each draw, DataTables sends (among others) draw, start (row offset), length (page size), order (column index and direction), and search[value]. Your endpoint must:
- Translate
orderinto a whitelistedORDER BY— never interpolate the column index into SQL without mapping it to a known column name. - Apply the search filter to both the page query and the filtered count query.
- Return JSON shaped as
{ "draw": <echoed draw>, "recordsTotal": N, "recordsFiltered": M, "data": [ ...one page of rows... ] }.
A minimal SQL sketch (pseudocode; parameterize everything):
-- page query
SELECT id, customer, total FROM orders
WHERE customer ILIKE '%' || :search || '%' -- only when search is non-empty
ORDER BY <whitelisted column> <dir>
LIMIT :length OFFSET :start;
-- filtered count (same WHERE clause as the page query)
SELECT COUNT(*) FROM orders WHERE customer ILIKE '%' || :search || '%';
-- total count (no filter)
SELECT COUNT(*) FROM orders;
How to verify the result
- Open browser dev tools, change pages, and confirm each change fires an Ajax request with
start/lengthand that the response body contains only one page of rows — not the whole table. - Compare
recordsTotalandrecordsFilteredagainst directCOUNT(*)queries with and without the search filter. They must match exactly. - Request a deep page (set
startnear the row count) and time the query. If it is slow, that is your signal to consider keyset pagination or better indexing on the ordered column.
Limitations. Exact parameter names and initialization syntax differ between DataTables 1.x and 2.x and between the jQuery and newer constructor styles — check the manual for your installed version. Search behavior under server-side mode will not match DataTables' client-side search unless you deliberately replicate it, which can surprise users migrating an existing table.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.