Laravel API Pagination: How to Avoid Slow Responses and High Memory Usage
A Laravel API can feel extremely fast during development and become noticeably slower once real users and real data arrive.
One common reason is simple:
The API is retrieving or processing more data than it needs.
An endpoint that performs well with 100 database records may behave very differently when the same table contains 100,000 or 1,000,000 records.
The problem becomes worse when every record loads relationships, executes additional queries, calculates expensive attributes, or returns fields the client never uses.
Pagination helps control this growth.
But adding paginate() to a Laravel query does not automatically make an API efficient.
The pagination method, database queries, indexes, relationships, response structure, filters, and frontend requirements all affect performance.
In this guide, we'll look at practical ways to build efficient Laravel APIs using paginate(), simplePaginate(), cursorPaginate(), eager loading, database indexes, API Resources, page-size limits, and performance testing.
Whether you're building a SaaS platform, mobile backend, admin dashboard, marketplace, or internal business application, these techniques can help keep your API responsive as the underlying data grows.
Why API Pagination Matters
Consider a basic orders endpoint:
GET /api/orders
A simple implementation might retrieve every order:
This may work perfectly during development.
The problem is that Order::all() retrieves the complete result set.
As the table grows, that can increase:
- Database execution time
- PHP memory consumption
- JSON serialization work
- Network payload size
- Browser or mobile processing
- Overall API response time
The client may also receive thousands of records it never displays.
A better starting point is a bounded response:
Instead of returning every order, the endpoint now returns a manageable page.
That's an improvement, but efficient pagination requires more than choosing a page size.
1. Understand Laravel's Three Main Pagination Methods
Laravel provides several pagination options.
The three you'll commonly encounter are:
paginate()simplePaginate()cursorPaginate()
They solve similar problems, but they work differently.
Choosing between them depends on:
- How large the dataset can become
- Whether the frontend needs total record counts
- Whether users need numbered pages
- How frequently the underlying data changes
- How users navigate through the results
Let's look at each option.
2. Use paginate() for Traditional Numbered Pages
Laravel's standard paginator is useful when the interface needs numbered pages.
This works well for interfaces that display navigation such as:
Previous | 1 | 2 | 3 | 4 | Next
It can also provide information such as:
Showing 1–25 of 1,000 results
That makes paginate() useful for admin dashboards, customer lists, order management, product catalogs, search interfaces, and business reporting screens.
The Trade-Off
Traditional pagination generally needs to determine how many records match the query so it can calculate the total number of pages.
For many applications, this is perfectly acceptable.
For very large or complicated queries, however, calculating that total can add database work.
Before using numbered pagination everywhere, ask:
Does the user actually need the total number of pages?
If not, Laravel provides another option.
3. Use simplePaginate() When Total Pages Aren't Needed
Imagine an activity feed.
The user only needs:
Previous | Next
They don't necessarily need to know that they are on page 1 of 2,436.
In this situation, simplePaginate() may be appropriate.
Simple pagination does not provide the same full total-count information as standard pagination.
That can avoid unnecessary count work for certain queries.
Good Candidates for simplePaginate()
Consider it for:
- Activity feeds
- Notifications
- Audit logs
- Transaction histories
- Administrative logs
- Search results where exact totals aren't important
Use paginate() when numbered pages and totals genuinely improve the user experience.
Use simplePaginate() when previous and next navigation is enough.
4. Understand the Cost of Deep Offset Pagination
Traditional pagination commonly relies on database offsets.
Conceptually, a deep page may require something similar to:
The database needs to find the correct position before returning the requested rows.
As users move deeper into a very large dataset, offset-based pagination can become less efficient depending on the database engine, query structure, available indexes, sorting, filters, and data distribution.
There is another issue.
Imagine a user loads page one. While they're reading it, several new records are inserted.
When they request page two, the position of existing records may have shifted. This can sometimes result in duplicated or skipped items during navigation.
For many ordinary dashboards, this isn't a serious problem.
For large or frequently changing datasets, however, cursor pagination may be a better option.
5. Use cursorPaginate() for Large Sequential Datasets
Laravel supports cursor pagination.
Instead of repeatedly asking the database to skip an increasingly large number of rows, the next request can continue from a known position.
Conceptually, a query might behave more like:
For suitable queries and indexes, this can make navigation through large datasets more efficient.
Cursor Pagination Can Be Useful For
- Infinite scrolling
- Activity streams
- Large order histories
- Event logs
- Notifications
- Large administrative datasets
- Continuously changing feeds
Cursor pagination requires appropriate ordering.
For example:
uses a straightforward unique ordering.
Cursor pagination can be powerful, but it isn't automatically the right choice for every endpoint.
6. Pagination Does Not Fix N+1 Queries
This is one of the most important things to understand about Laravel API performance.
Consider:
At first glance, the endpoint looks efficient because it only retrieves 25 orders.
But imagine that your response accesses the customer relationship for every order:
If customer hasn't already been loaded, Laravel may execute additional database queries while processing those orders.
This is the N+1 query problem.
You might execute one query to retrieve the orders and many additional queries to retrieve related customers.
If the relationship is required, eager loading may help:
Now Laravel can retrieve the required relationship more efficiently.
However, there is another trap.
Don't solve N+1 problems by blindly loading every relationship:
That may be unnecessarily expensive if the endpoint only displays the customer name, order total, and status.
Load the relationships your response actually needs.
7. Return Only the Fields the Client Needs
Suppose an orders page displays:
- Order ID
- Customer name
- Total
- Status
- Creation date
The endpoint may not need every column from the orders table.
You can select the fields required by that response:
This can reduce unnecessary data transfer between the database and application.
It can also reduce the amount of information that needs to be serialized into JSON.
Be careful when selecting columns required by relationships. If Laravel needs customer_id to resolve the customer relationship, removing it can prevent that relationship from working correctly.
Optimization should always preserve correctness.
8. Database Indexes Matter More as Data Grows
Pagination cannot compensate for an inefficient database query.
Consider this endpoint:
When the table contains a small number of records, the query may be fast.
As the dataset grows, appropriate database indexes become increasingly important.
Depending on the application's query patterns, useful indexes may involve columns used for filtering, sorting, joins, foreign keys, and frequently combined conditions.
Don't add indexes blindly.
Use your database's query analysis tools, such as EXPLAIN, to understand how important queries are executed.
An index can improve read performance, but indexes also consume storage and add work to inserts, updates, and deletes.
Design indexes around real query patterns rather than indexing every available column.
9. Always Limit per_page
A paginated endpoint can still create excessive load if clients control page size without limits.
Imagine this request:
GET /api/orders?per_page=100000
If your application accepts that value directly, the client has effectively bypassed your pagination strategy.
Instead, enforce a server-side maximum:
Now the client can request a preferred page size, but the server remains in control.
The correct maximum isn't always 100.
It depends on record size, number of relationships, query complexity, response structure, server resources, and client requirements.
Measure realistic workloads and choose an appropriate limit for your application.
10. Validate Sorting and Filtering Parameters
APIs frequently allow clients to choose sorting.
For example:
GET /api/orders?sort=created_at
Avoid directly passing arbitrary client input into query structure.
Instead, define allowed fields:
The same principle applies to:
- Filters
- Search fields
- Relationship includes
- Export columns
- Sort directions
Allowlisting keeps your API behavior predictable and reduces unnecessary risk.
11. Use API Resources to Control the Response
Returning complete Eloquent models directly may expose fields the frontend doesn't need.
Laravel API Resources provide explicit control over your response structure.
Then:
This gives you an explicit API contract.
It can help prevent accidental field exposure, keep responses consistent, reduce unnecessary response data, separate database models from public API structures, and make future API changes easier to manage.
12. Watch for Expensive Accessors and Computed Fields
An endpoint can execute a fast database query and still produce a slow response.
Why?
Because each model may perform additional work while being converted into JSON.
An accessor or computed field might:
- Execute another database query
- Perform a complicated calculation
- Process a large text field
- Read from storage
- Transform an image
- Call another service
Imagine one expensive calculation takes 50 milliseconds.
If it runs for 25 records, you've potentially added significant processing time to a single request.
Review your accessors, appended attributes, API Resources, relationship methods, model events, and custom serialization logic.
Frequently used API endpoints should remain inexpensive to serialize.
13. Avoid External API Calls for Every Record
Imagine your endpoint returns 25 products.
While building the response, the application requests live inventory information from an external service separately for every product.
One incoming API request could trigger:
1 client request → 25 external requests → 1 final response
Your database might be fast while the endpoint itself remains slow and fragile.
Depending on the business requirements, better architectures can include:
- Cached external data
- Background synchronization
- Batch API requests
- Queue jobs
- Locally maintained data projections
External network requests should be designed deliberately rather than hidden inside model serialization.
14. Be Careful With Search and Pagination
Search can become another bottleneck.
A simple product search might use:
This can work well for a small catalog.
As the dataset and search requirements grow, you may need to evaluate database indexing, full-text search, database-specific search capabilities, or dedicated search infrastructure.
Don't introduce additional search infrastructure simply because your table has grown.
First measure the existing query and identify the real bottleneck.
Additional infrastructure introduces additional operational complexity.
15. Cache Paginated Responses Carefully
Caching can improve frequently requested endpoints, but paginated APIs require careful cache design.
Consider the possible combinations:
- Page 1
- Page 2
- Paid orders
- Pending orders
- Different sorting
- Different searches
- Different users
- Different tenants
A poorly designed cache strategy can create a large number of entries.
More importantly, private or tenant-specific results must never be shared through unsafe cache keys.
A cache key may need to account for the tenant, user or permission scope, page or cursor, filters, sorting, search query, and locale.
You also need a clear invalidation strategy when underlying data changes.
Never cache private information globally just to improve performance.
16. Don't Forget Nested Collections
Consider:
The customers are paginated.
But the orders relationship may not be.
If each customer has thousands of orders, the endpoint could still load a huge amount of data.
Ask whether the parent response genuinely needs the complete nested collection.
Often a cleaner design is:
GET /api/customers
and:
GET /api/customers/{customer}/orders
Now both collections can have their own pagination rules.
This keeps response sizes more predictable and makes each endpoint easier to optimize.
17. Measure Before You Optimize
Performance work should be based on evidence.
Useful questions include:
- How many database queries does this request execute?
- Which SQL query takes the longest?
- How much memory does the request consume?
- How large is the JSON response?
- How long does serialization take?
- Are external services involved?
- How does the endpoint behave with realistic data?
- Does performance degrade on deeper pages?
Laravel development tools, application monitoring platforms, database profiling tools, and infrastructure metrics can help answer these questions.
Instead of saying:
“This API feels slow.”
Try to identify:
“Where is the request spending its time?”
That turns a vague performance problem into something measurable.
18. Test With Realistic Data
An endpoint containing 50 development records tells you very little about how it will behave when the production table contains 500,000 records.
Before launching an important API, test with representative data volumes.
Measure:
- First-page performance
- Deep-page performance
- Common filters
- Sorting
- Search queries
- Concurrent requests
- Memory consumption
- Response payload size
Avoid copying sensitive customer data into insecure development environments.
Use generated or appropriately sanitized data whenever possible.
paginate() vs simplePaginate() vs cursorPaginate()
| MethodBest ForTotal CountNavigation | |||
paginate() | Traditional dashboards and numbered pages | Yes | Numbered pages |
simplePaginate() | Lists where totals aren't needed | No full total | Previous / Next |
cursorPaginate() | Large sequential datasets and feeds | No traditional total | Cursor-based |
There is no universally best pagination method.
Choose the method that matches your interface and data characteristics.
Example: Building a More Efficient Orders Endpoint
Suppose a dashboard needs to display:
- Order ID
- Customer
- Total
- Status
- Creation date
A reasonable starting implementation could look like this:
This implementation:
- Limits response size
- Selects relevant fields
- Eager-loads the required relationship
- Enforces a maximum page size
- Uses deterministic ordering
- Uses a Resource to control the public response
It is still only a starting point.
The right implementation depends on your database schema, available indexes, filtering requirements, authorization model, traffic patterns, and frontend requirements.
Laravel API Pagination Checklist
| CheckStatus | |
| Response size is bounded | ☐ |
Maximum per_page value is enforced | ☐ |
| Pagination strategy matches the interface | ☐ |
| N+1 queries have been checked | ☐ |
| Required relationships are eager-loaded | ☐ |
| Unnecessary relationships are excluded | ☐ |
| Only necessary fields are returned | ☐ |
| Important database queries have appropriate indexes | ☐ |
| Sorting parameters are validated | ☐ |
| Filtering parameters are validated | ☐ |
| API response fields are explicitly controlled | ☐ |
| Expensive accessors have been reviewed | ☐ |
| Nested collections are bounded | ☐ |
| User and tenant authorization is enforced | ☐ |
| Endpoint has been tested with realistic data | ☐ |
Only mark an item complete after it has actually been verified.
Common Laravel Pagination Mistakes
Returning Every Record
Using Model::all() for an unbounded dataset can become increasingly expensive as the application grows.
Allowing Unlimited Page Sizes
Don't trust arbitrary per_page values supplied by clients.
Eager-Loading Everything
Eager loading helps solve N+1 problems, but loading relationships the endpoint doesn't use creates unnecessary work.
Ignoring Database Indexes
A smaller response doesn't automatically mean the underlying query is efficient.
Returning Unnecessary Fields
Twenty-five records can still produce a large response when each record contains large fields and unnecessary relationships.
Performing External Requests During Serialization
Network requests for every record can make response times unpredictable.
Assuming Cursor Pagination Solves Every Problem
Cursor pagination does not fix N+1 queries, missing indexes, expensive searches, slow external services, excessive serialization, or poor database design.
It solves a specific pagination problem.
How Pagination Fits Into Laravel Production Readiness
API pagination is only one part of preparing a Laravel application for real-world use.
A production application should also be reviewed for:
- Security
- Authentication and authorization
- Database performance
- Queues
- Caching
- Logging
- Monitoring
- Backups
- Deployment safety
For the complete production review, read our [Laravel Production Readiness Checklist: 25 Things to Verify Before Launch](/blog/laravel-production-readiness-checklist).
That guide covers the broader production concerns surrounding your Laravel application.
Final Thoughts
Laravel makes pagination easy to implement.
Building pagination that continues working efficiently as an application grows requires more thought.
Start by answering four questions:
- Does the interface actually need a total record count?
- How large could the dataset become?
- Which relationships and fields does the client genuinely need?
- What do measurements show is making the endpoint slow?
Then choose the simplest pagination strategy that satisfies those requirements.
For many applications, ordinary paginate() is perfectly appropriate.
For interfaces that only need previous and next navigation, simplePaginate() can avoid unnecessary total-count information.
For large sequential datasets and infinite scrolling, cursorPaginate() may be a better fit.
Most importantly, combine pagination with efficient queries, appropriate indexes, bounded response sizes, controlled serialization, authorization, and realistic performance testing.
A fast API isn't created by one Laravel method.
It comes from understanding how the complete request moves through your application, database, infrastructure, and response layer.
Is Your Laravel API Getting Slower as Your Data Grows?
Slow API responses aren't always caused by the server itself.
The bottleneck may be database queries, N+1 relationships, excessive payloads, inefficient pagination, serialization, external services, or a combination of several issues.
CodexiLab helps businesses build and improve APIs, backend systems, SaaS applications, and production software.
If you're dealing with slow responses, excessive queries, high memory usage, or an API that isn't scaling as expected, we can help investigate the architecture and identify the bottlenecks.
Disclaimer: The examples in this article are general engineering guidance. Production requirements vary depending on your Laravel version, database, infrastructure, application architecture, security requirements, and workload.