Why is your Strapi API slow?
In most cases it isn't Strapi. It's unbounded population. A request using populate=* or the populate=deep plugin fetches the entire content tree for every entry, so response times grow with your schema rather than with what the page actually needs. Before changing anything, confirm the slowness is in the API at all: request the endpoint directly and compare it against what your frontend is doing.
00· Before anything
First: prove it's the API
"Strapi is slow" is reported far more often than it's true. A page that takes four seconds to render might be one slow Strapi request, or twelve fast ones fired sequentially, or an unoptimised image, or a cold serverless function.
Request the endpoint on its own, away from the frontend:
curl -w "\ntotal: %{time_total}s\n" -o /dev/null -s \
"https://your-api/api/articles?populate=*"Then run the same request with explicit population of only the fields that page needs. If the second is dramatically faster, you have a population problem and the rest of this page applies. If both are fast, the problem is in front of Strapi and you're looking in the wrong place.
Do this before you add Redis. Caching a query you should have fixed hides the problem until the cache misses.
Likely causes · Most → least common
Cause 01 of 05
Unbounded population
Response time scales with your schema rather than with page size. Adding an unrelated content type makes existing endpoints slower. Payloads are large and contain fields the frontend never reads.
Compare populate=* against explicit population on the same endpoint. Check the response size, not just the time: wc -c on the body is often more revealing.
populate=* fetches one level of every relation, component, media field and dynamic zone. The populate=deep community plugin goes further and removes the depth ceiling entirely, which means the cost of every request changes each time someone adds a relation, and nobody notices until it's in production.
Explicit population, scoped to what the page renders:
/api/articles?fields[0]=title&fields[1]=slug
&populate[cover][fields][0]=url
&populate[cover][fields][1]=alternativeText
&populate[author][fields][0]=nameThe Strapi 5 documentation recommends limiting population depth to two or three levels in production. Beyond that, question the content model rather than the query.
Two things worth knowing: fields has no effect on relational, media, component or dynamic zone fields. You scope those inside their own populate block. And large populate lists are bounded by the query parser's arrayLimit, which defaults to 100.
Don't leave population choices to the frontend. Route-level middleware centralises them, so query cost is a decision made once in the API rather than re-litigated in every component. I've written up the pattern in more detail in Building high-performance Strapi applications on strapi.io.
Cause 02 of 05
The frontend is making too many requests
Every individual request is fast. The page still takes seconds.
Count the requests to /api/ for a single page load. If one page is making eight or twelve calls, the problem is request count, not query speed.
Consolidate into a single populated query. This is the classic N+1: fetching a list, then fetching each item's relations one at a time. It looks like a Strapi problem because every entry in the log is a Strapi request.
Cause 03 of 05
Missing indexes on filtered and sorted fields
Unfiltered endpoints are fine. Filtered or sorted ones are slow, and getting slower as the table grows.
Run the underlying query directly against Postgres with EXPLAIN ANALYZE. A sequential scan on a large table is your answer.
Index the columns you actually filter and sort on: slug, publication date, locale, whatever your list endpoints key off. Index deliberately rather than broadly; every index costs you on write.
Cause 04 of 05
Unpaginated collections
Degradation is linear over time. The endpoint was fine at launch and nobody changed anything.
Check the entry count on the collection and whether the request sets a page size.
Paginate every list endpoint, with a page size the frontend genuinely renders. An endpoint with no limit is a slow query waiting for enough content.
Cause 05 of 05
Everything hits the database
Uniformly slow, consistent, no obvious outliers. Content changes rarely but is re-queried constantly.
Cache last, and only once the queries underneath are right. Response caching for GET endpoints, CDN caching for anything genuinely public, and tag-based invalidation from Strapi webhooks if your frontend uses ISR, so publishing invalidates immediately rather than waiting out a timer.
06
If it's none of these
Check the unglamorous things. Node 20 or later is a hard requirement for Strapi 5: schema.tables.toSorted is not a function is the canonical symptom of a Node version that's too old. Confirm your database isn't on a starved instance or exhausting its connection pool under load. And check whether slowness correlates with draft-and-publish status queries, which add cost that's easy to miss.
There are open issues worth reading if your case doesn't match anything here: #13288 on populate=* response times and #23168 on general API slowness both have useful discussion.
Common questions
In local development, when you want to see the shape of your data. Not in production, where it makes query cost a function of your schema rather than a decision you made.
Two or three levels. If a page genuinely needs more, that's usually a content modelling problem rather than a query problem.
It will hide one. Fix the query first, then cache it. Otherwise every cache miss is a production incident waiting for the right traffic pattern.
Different, not automatically faster. The Document Service API changed how queries are composed, so v4 optimisations don't always transfer, and Node 20+ is required.
Diagnosed everything and still stuck?
S/01 — Performance & Architecture Rescue.
A fixed-scope week that finds the cause and fixes what's causing it.