← All writing
Performance · 2 June 2026 · 3 min read

Making a Laravel API 20% faster without a rewrite

Measure first, then fix the usual suspects: N+1 queries, missing indexes, over-fetching, offset pagination and work that belongs in a queue.
Imron Athoriq Firjatulloh
Backend Engineer · PT Serasi Autoraya (Astra International)
When an API gets slow, the tempting answer is a rewrite in something faster. In my experience the language is almost never the problem. On the platform I work on, a round of refactoring and API optimisation improved system performance by about 20%, and none of it needed a new framework. It needed measurement and a short list of fixes.

1. Measure before touching anything

Pick the endpoints that matter (the ones users wait on, and the ones that run most often) and record their p95 latency and query count per request. Averages hide the slow requests that users complain about.
In development, Laravel Telescope or Debugbar shows every query a request makes. In production, log the duration and query count of each request:
php
// AppServiceProvider::boot() DB::listen(function ($query) { if ($query->time > 200) { Log::warning('slow query', ['sql' => $query->sql, 'ms' => $query->time]); } });
Now you have a baseline, and every change can be judged against it.

2. Kill the N+1 queries

The classic: a list endpoint loads 50 auctions, then the resource accesses $auction->vehicle and $auction->highestBid for each one. That is 101 queries for one page.
php
// before: 1 + 50 + 50 queries $auctions = Auction::latest()->paginate(50); // after: 3 queries $auctions = Auction::with(['vehicle', 'highestBid'])->latest()->paginate(50);
Add Model::preventLazyLoading(! app()->isProduction()); so new N+1s fail loudly in development instead of being discovered by users.

3. Index what you filter and sort by

Run EXPLAIN on the slow queries from step 1. A type: ALL with a large rows estimate is a full table scan. A composite index should match the query's shape: equality columns first, then the range or sort column.
sql
-- WHERE status = 'live' ORDER BY ends_at CREATE INDEX ix_auctions_status_ends ON auctions (status, ends_at);
Do not index everything: every index slows down writes. Index the queries you measured.

4. Fetch less

select * on a table with large text columns moves data nobody uses. Select the columns the endpoint returns, and use API resources so the response shape is explicit. For counts, use withCount('bids') instead of loading every bid to count them in PHP.

5. Replace deep offset pagination

OFFSET 50000 makes the database read and throw away 50,000 rows. For feeds and exports, use keyset (cursor) pagination: "give me the next 50 after this id".
php
$page = Bid::where('auction_id', $id)->orderByDesc('id')->cursorPaginate(50);

6. Cache reference data, carefully

Lists of vehicle makes, branches or document types change rarely and are read constantly. Cache them with a clear key and invalidate on write. Do not cache anything whose staleness costs money, such as the current price of a live auction.

7. Move slow work out of the request

Sending emails, generating PDFs and calling third-party APIs do not need to finish before the response. Dispatch a queued job and return. The user gets a fast answer, and a failure can be retried without the user resubmitting.

8. Check the result, then keep it

Compare p95 and query counts with the baseline. Then protect the gain: a test that asserts the query count of key endpoints, and the slow-query log from step 1 left on, turn a one-off clean-up into a standard the team keeps.
Most "the framework is slow" problems are "this request makes 300 queries" problems. Count the queries first.
© 2026 Imron Athoriq Firjatulloh · Built with React Native & Expo
Home →