Large ACF Repeater fields (100+ rows per post) slow down WordPress because every sub-field on every row is its own wp_postmeta row. A Repeater with 200 rows and 5 sub-fields per row is 1,000+ meta rows for a single post. Without a persistent object cache, every page render that touches that field issues that many queries. The fix is rarely "make ACF faster"; it is "add an object cache, reduce reads, or move the data to a structure that scales better." Here is what each of those looks like in practice.
Jump to:
- Why Repeaters explode the meta count
- The actual symptom in production
- Fix 1: persistent object cache (Redis or Memcached)
- Fix 2: read the Repeater once, not many times
- Fix 3: count without reading rows
- Fix 4: move the data out of Repeater
- The rule of thumb on Repeater size
Why Repeaters explode the meta count
ACF Repeaters are stored as serialized meta in a specific shape. For a Repeater field named team_members with 3 rows and sub-fields name, role, photo, the wp_postmeta table looks like:
post_id | meta_key | meta_value
--------|-----------------------------|----------------------------
42 | team_members | 3
42 | _team_members | field_team_members_key
42 | team_members_0_name | "Alex Chen"
42 | _team_members_0_name | field_name_key
42 | team_members_0_role | "Lead Engineer"
42 | _team_members_0_role | field_role_key
42 | team_members_0_photo | 42
42 | _team_members_0_photo | field_photo_key
42 | team_members_1_name | "Sam Patel"
42 | _team_members_1_name | field_name_key
...
Each sub-field on each row gets two rows: one with the value, one with the ACF field key reference (used for the field-key system). So a Repeater with 3 rows and 3 sub-fields = 18 meta rows (3 × 3 × 2), plus the field count and the field key reference at the top (2 more) = 20 meta rows total for one Repeater field on one post.
Scale that up: 100 rows × 5 sub-fields × 2 = 1,000 meta rows per Repeater per post.
This is by design (it makes individual sub-fields queryable via standard WordPress meta APIs), but it explodes the row count.
The actual symptom in production
On a cold cache (first request, no object caching), a page that reads the Repeater issues:
- One query for
get_post_meta( $post_id, 'team_members', true )(the count). - One query for each
_team_membersreference. - One query per sub-field access via
get_sub_field(), unless ACF's internal request-scoped cache catches it.
For a 200-row Repeater with 5 sub-fields, the first cold-cache render can hit 1,000+ queries. Page render time at that scale is measured in seconds, not milliseconds.
On a warm cache (second request), WordPress's wp_postmeta cache stores the result of get_post_meta( $post_id, '', '' ) (read all meta for the post) and subsequent reads are free. So the second request renders fast.
The real-world problem is the first cold-cache hit on every post. If your site has lots of unique URLs that get crawled rarely (long-tail content, archive pages, listing detail pages on directory sites), most requests are cold-cache requests.
Fix 1: persistent object cache (Redis or Memcached)
This is the single highest-impact fix. With a persistent object cache (Redis is the modern default), WordPress caches the entire wp_postmeta for each post across requests. The first request still pays the meta-read cost, but every subsequent request anywhere on the site reads from Redis instead of MySQL.
Managed WordPress hosts (Kinsta, WP Engine, Pressable, Rocket.net) ship with object caching by default. Self-managed setups need to install Redis and the redis-cache plugin (free).
Verification:
wp redis status # if you have the wp-cli-redis plugin
wp option get _transient_my_test # set and read a transient to confirm cache worksAfter enabling object cache, the same large-Repeater page that took 2.5 seconds cold should drop to ~250ms warm. Persistent across requests because Redis persists across requests.
This single change has resolved 80% of the "Repeater is slow" tickets I have debugged.
Fix 2: read the Repeater once, not many times
A common template-code mistake: calling get_field('team_members') multiple times in the same request (once for the count, once for the loop, once for the "show all" toggle, etc.). ACF caches it internally, but the cleaner pattern is to read once and pass the array around:
$members = get_field( 'team_members' ) ?: [];
$count = count( $members );
if ( $count > 0 ) {
foreach ( $members as $i => $member ) {
// render $member['name'], $member['role'], etc.
}
}This avoids the function call overhead and makes the data dependency explicit. For Repeaters with many sub-fields per row, this is meaningfully faster than the have_rows/the_row/get_sub_field pattern, which has more internal bookkeeping.
The trade-off: you lose the implicit row context that have_rows provides, so you cannot use setup_postdata or other context-aware helpers inside the foreach. For most rendering loops this is fine.
Fix 3: count without reading rows
When you only need the count (and not the data), read the count directly from the meta key rather than loading the full Repeater:
$count = (int) get_post_meta( $post_id, 'team_members', true );This is one meta read instead of N. Covered in detail in How to Count Rows in an ACF Repeater Field. Common use case: showing a "View N team members" link on a listing index page where the actual member data only loads when the user clicks through.
On a directory listing index showing 50 listings each with 10-50 team members, this saves 500-2,500 meta reads per page render.
Fix 4: move the data out of Repeater
For genuinely large datasets (200+ rows, growing over time, frequently queried), the Repeater is the wrong storage structure. Alternatives:
Separate post type + Relationship. Make each "team member" its own post. The parent post has a Relationship field pointing at member posts. Each member is queryable independently via WP_Query, can have its own URL, can have its own meta, scales linearly with no meta-row explosion. This is the right choice when the row data is structured enough to warrant first-class status.
Custom database table. For ultra-high-scale data (10,000+ rows per post, frequently filtered, no individual addressability needed), a custom table queried via $wpdb is the right answer. Bypasses wp_postmeta entirely. More setup; vastly more scalable.
Taxonomy term meta. If the data is genuinely categorical (a list of countries, a list of industries), it might belong as terms in a custom taxonomy rather than as Repeater rows. Term meta is its own meta table and is much smaller than wp_postmeta.
The right choice depends on the access pattern. Repeater is for content that:
- Belongs editorially to the parent post (does not exist independently).
- Is read all-at-once when the parent is rendered.
- Is small (under ~50 rows is comfortable).
Repeater is the wrong choice for data that violates any of those.
The rule of thumb on Repeater size
After many years of running ACF on production directory sites, the size buckets I work with:
- Under 20 rows: Repeater is fine, no special concern.
- 20-50 rows: Repeater is fine if the site has object caching.
- 50-100 rows: Repeater is fine but start watching query counts. Consider Fix 2 (read once) and Fix 3 (direct count).
- 100-500 rows: Repeater works but is no longer the right tool. Consider Fix 4 (separate post type).
- 500+ rows: Repeater is wrong. Move the data.
The "right" size depends on the site's traffic and hosting, but these are the brackets I use as decision points. For the broader pattern of detecting and fixing ACF performance issues at scale, see Common ACF Performance Problems on Large WordPress Sites and Too Many ACF Fields? Here is When WordPress Starts Struggling. For the WP-CLI patterns that audit Repeater sizes across a site, see Using AI with WP-CLI for Faster WordPress Operations.
Sources
Authoritative references this article was fact-checked against.
- Repeater field (Advanced Custom Fields docs)advancedcustomfields.com
- get_post_meta() function reference (WordPress Developer Resources)developer.wordpress.org





