Fieldstone vs ACF: WordPress custom field database benchmark
ACF stores every custom field value in wp_postmeta. Fieldstone stores one row per post in its own table, with a real column for each field. We put the same 10,000 posts and 100,000 field values into both and timed the things a site actually does: querying by a field, reading values and saving them. The method, the exact SQL and the code are all on this page.
Medians of 20 runs, measured 2026-10-02 on a development laptop (Intel Core i5-9300H @ 2.40GHz, 8 GB RAM, SSD). Your numbers will differ; the code is here to check.
Results
Milliseconds, median of 20 runs; lower is better. Each pair of bars is scaled to the slower of the two, so compare bars within a test, not across tests. The 95th percentiles are in the tables further down.
Queries by field value
Time for the database to answer the query.
Reading values
Time through each plugin’s own functions: get_field() and fstn_get_field().
Saving values
Time through each plugin’s own functions: update_field() and fstn_update_field().
How to read these
- The indexed queries are the real result. With an index the database goes straight to the matching rows.
wp_postmetahas an index on the field name but not on the value, so it reads every row for that field (about 10,000 here) and compares each one. That gap grows with the number of posts. - The three-filter figure (374×) is a query-planner outcome, not a constant. Three
meta_queryclauses joinwp_postmetato itself three times, and how badly that goes depends on the plan the database picks — the plan it picked here is shown below. Read it as “this can get very slow”, and use the single-filter figure when you want one number. - Without an index, most of the advantage goes. Filtering on a Fieldstone column that has no index was 1.3× faster, and sorting far down the list (page 41 and beyond) was 1.7× faster, because the database stops using the index there. Fieldstone lets you index a field with one checkbox; it does not index everything for you.
- Saving existing values is slower by default. Fieldstone writes each value twice: to its own table and to a plain
wp_postmetacopy (the “mirror”) so that SEO, export and search plugins keep working. That took 59 SQL queries against ACF’s 30. With the mirror switched off the same save took 6.33 ms, which is 1.3× faster than ACF. - Fieldstone’s lead on reads is not a database win. Both plugins read one post’s fields with a single query, and on the archive page Fieldstone actually issued more queries (23 against ACF’s 3) because it fetches each post’s row separately. It was still quicker overall, so the difference there is time spent in PHP per field, not in SQL.
All the numbers
Median in milliseconds, with the 95th percentile in brackets. “Mirror off” is Fieldstone writing to its own table only.
Queries by field value
| Test | ACF | Fieldstone | Fieldstone, mirror off | Fieldstone vs ACF |
|---|---|---|---|---|
| Filter by one field city = X, newest 20 | 18.2 (20.0) | 1.03 (1.57) | 0.92 (0.99) | 17.6× faster |
| Numeric range price between A and B, newest 20 | 20.8 (23.0) | 1.97 (2.10) | 1.77 (2.93) | 10.6× faster |
| Three filters + total count city, bedrooms ≥ 3, featured, newest 20 | 465 (480) | 1.24 (1.35) | 0.78 (0.85) | 374× faster |
| Sort by a field, first pages price, pages 1–5 | 36.0 (38.4) | 0.41 (0.49) | 0.38 (0.46) | 87.2× faster |
| Sort by a field, deep pages price, page 41 onwards | 36.7 (39.8) | 21.2 (23.6) | 20.1 (21.8) | 1.7× faster |
| Filter on a column with no index agent email = X, newest 20 | 18.8 (19.8) | 14.7 (19.0) | 13.3 (17.3) | 1.3× faster |
Reading values
| Test | ACF | Fieldstone | Fieldstone, mirror off | Fieldstone vs ACF |
|---|---|---|---|---|
| Read 10 fields of one post cold cache | 0.77 (0.86) 1 query | 0.23 (0.25) 1 query | 0.24 (0.27) 1 query | 3.3× faster |
| Archive page query 20 posts, read 10 fields of each | 12.3 (13.4) 3 queries | 6.54 (7.15) 23 queries | 5.88 (6.49) 23 queries | 1.9× faster |
Saving values
| Test | ACF | Fieldstone | Fieldstone, mirror off | Fieldstone vs ACF |
|---|---|---|---|---|
| Save 10 changed fields on an existing post | 8.33 (9.09) 30 queries | 13.7 (15.6) 59 queries | 6.33 (7.27) 30 queries | 1.6× slower |
| First save of 10 fields on a new post | 15.6 (18.1) 60 queries | 13.8 (14.8) 60 queries | 6.59 (7.53) 30 queries | 1.1× faster |
Storage
What the 100,000 values occupy on disk, data plus indexes.
| postmeta rows | postmeta size | Table rows | Table size | Total | |
|---|---|---|---|---|---|
| ACF | 200,000 | 27.6 MB | – | – | 27.6 MB |
| Fieldstone | 100,000 | 14.5 MB | 10,000 | 5.0 MB | 19.6 MB |
| Fieldstone, mirror off | 0 | 0.0 MB | 10,000 | 5.0 MB | 5.1 MB |
How it was measured
Each plugin was tested in its own PHP process with the other plugin not loaded, on a WordPress install with nothing else active.
- Same data. 10,000 posts with 10 fields each: a city, a price, bedrooms, bathrooms, area, a featured flag, a status, a date, an email and a short text. The values are derived from each post’s number, not generated randomly, so both plugins store exactly the same thing.
- Written the normal way. Values go in through
update_field()andfstn_update_field(), then a sample is read back and compared with what was written. - Same answers. Every query test records which posts came back, in order. Both plugins returned identical posts on every test; the page generator refuses to build otherwise.
- Warm-up, then 20 timed runs. 3 runs are thrown away first. Runs rotate through different filter values and pages.
- Median and 95th percentile. One slow run should not move the headline number.
The ACF queries are whatever WP_Query generates for a meta_query, which is the documented way to query ACF data. Timing them through WP_Query itself instead of as raw SQL changed no median by more than 2%; both figures are in the raw data.
The Fieldstone queries are SQL against Fieldstone’s table, joined to wp_posts with the same post type, status, order and limit. Fieldstone indexes the four fields these tests filter or sort by, which is a per-field checkbox in its editor. One test deliberately filters on a column with no index.
Because the variants run minutes apart, each one also times a fixed workload that involves neither plugin (a SELECT 1 round trip and a CPU loop). In this run those were 0.080 ms and 26.3 ms during the ACF variant and 0.074 ms and 27.5 ms during the Fieldstone variant, so the machine was running at the same speed for both. The page generator refuses to build from a run where they differ by more than 25%.
The whole benchmark was run a second time, with the variants in a different order to rule out the order mattering. Every ACF median was within 10% of this run and every Fieldstone median within 23% (Fieldstone’s sub-millisecond timings move more in relative terms). Both runs are in the download.
The SQL that was timed
One example per test, with the plan the database chose for it.
Filter by one field
ACF, as generated by WP_Query
SELECT wp_posts.ID FROM wp_posts INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id ) WHERE 1=1 AND ( ( wp_postmeta.meta_key = 'bench_city' AND wp_postmeta.meta_value = 'Austin' ) ) AND wp_posts.post_type = 'fstn_bench' AND ((wp_posts.post_status = 'publish')) GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC LIMIT 0, 20Plan: wp_postmeta: ref on meta_key, about 17750 rows (Using where; Using temporary; Using filesort) → wp_posts: eq_ref on PRIMARY, about 1 row (Using where)
Fieldstone
SELECT p.ID FROM wp_posts p INNER JOIN wp_fstn_data_bench_listing d ON d.object_id = p.ID AND d.object_type = 'post' WHERE p.post_type = 'fstn_bench' AND p.post_status = 'publish' AND d.bench_city = 'Austin' ORDER BY p.post_date DESC LIMIT 20Plan: d: ref on bench_city, about 202 rows (Using index condition; Using where; Using temporary; Using filesort) → p: eq_ref on PRIMARY, about 1 row (Using where)
Numeric range
ACF, as generated by WP_Query
SELECT wp_posts.ID FROM wp_posts INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id ) WHERE 1=1 AND ( ( wp_postmeta.meta_key = 'bench_price' AND CAST(wp_postmeta.meta_value AS SIGNED) BETWEEN '100000' AND '200000' ) ) AND wp_posts.post_type = 'fstn_bench' AND ((wp_posts.post_status = 'publish')) GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC LIMIT 0, 20Plan: wp_postmeta: ref on meta_key, about 17350 rows (Using where; Using temporary; Using filesort) → wp_posts: eq_ref on PRIMARY, about 1 row (Using where)
Fieldstone
SELECT p.ID FROM wp_posts p INNER JOIN wp_fstn_data_bench_listing d ON d.object_id = p.ID AND d.object_type = 'post' WHERE p.post_type = 'fstn_bench' AND p.post_status = 'publish' AND d.bench_price BETWEEN 100000 AND 200000 ORDER BY p.post_date DESC LIMIT 20Plan: d: range on bench_price, about 525 rows (Using index condition; Using where; Using temporary; Using filesort) → p: eq_ref on PRIMARY, about 1 row (Using where)
Three filters + total count
ACF, as generated by WP_Query
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id ) INNER JOIN wp_postmeta AS mt1 ON ( wp_posts.ID = mt1.post_id ) INNER JOIN wp_postmeta AS mt2 ON ( wp_posts.ID = mt2.post_id ) WHERE 1=1 AND ( ( wp_postmeta.meta_key = 'bench_city' AND wp_postmeta.meta_value = 'Austin' ) AND ( mt1.meta_key = 'bench_bedrooms' AND CAST(mt1.meta_value AS SIGNED) >= '3' ) AND ( mt2.meta_key = 'bench_featured' AND mt2.meta_value = '1' ) ) AND wp_posts.post_type = 'fstn_bench' AND ((wp_posts.post_status = 'publish')) GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC LIMIT 0, 20Plan: wp_posts: ref on type_status_date, about 5032 rows (Using where; Using index; Using temporary; Using filesort) → mt1: ref on post_id, about 10 rows (Using where) → wp_postmeta: ref on post_id, about 10 rows (Using where) → mt2: ref on post_id, about 10 rows (Using where)
Fieldstone
SELECT SQL_CALC_FOUND_ROWS p.ID FROM wp_posts p INNER JOIN wp_fstn_data_bench_listing d ON d.object_id = p.ID AND d.object_type = 'post' WHERE p.post_type = 'fstn_bench' AND p.post_status = 'publish' AND d.bench_city = 'Austin' AND d.bench_bedrooms >= 3 AND d.bench_featured = 1 ORDER BY p.post_date DESC LIMIT 20Plan: d: index_merge on bench_featured,bench_city, about 20 rows (Using intersect(bench_featured,bench_city); Using where; Using temporary; Using filesort) → p: eq_ref on PRIMARY, about 1 row (Using where)
Sort by a field, first pages
ACF, as generated by WP_Query
SELECT wp_posts.ID FROM wp_posts INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id ) WHERE 1=1 AND ( wp_postmeta.meta_key = 'bench_price' ) AND wp_posts.post_type = 'fstn_bench' AND ((wp_posts.post_status = 'publish')) GROUP BY wp_posts.ID ORDER BY wp_postmeta.meta_value+0 DESC LIMIT 0, 20Plan: wp_postmeta: ref on meta_key, about 17350 rows (Using where; Using temporary; Using filesort) → wp_posts: eq_ref on PRIMARY, about 1 row (Using where)
Fieldstone
SELECT p.ID FROM wp_posts p INNER JOIN wp_fstn_data_bench_listing d ON d.object_id = p.ID AND d.object_type = 'post' WHERE p.post_type = 'fstn_bench' AND p.post_status = 'publish' ORDER BY d.bench_price DESC LIMIT 0, 20Plan: d: index on bench_price, about 80 rows (Using where) → p: eq_ref on PRIMARY, about 1 row (Using where)
Sort by a field, deep pages
ACF, as generated by WP_Query
SELECT wp_posts.ID FROM wp_posts INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id ) WHERE 1=1 AND ( wp_postmeta.meta_key = 'bench_price' ) AND wp_posts.post_type = 'fstn_bench' AND ((wp_posts.post_status = 'publish')) GROUP BY wp_posts.ID ORDER BY wp_postmeta.meta_value+0 DESC LIMIT 800, 20Plan: wp_postmeta: ref on meta_key, about 17350 rows (Using where; Using temporary; Using filesort) → wp_posts: eq_ref on PRIMARY, about 1 row (Using where)
Fieldstone
SELECT p.ID FROM wp_posts p INNER JOIN wp_fstn_data_bench_listing d ON d.object_id = p.ID AND d.object_type = 'post' WHERE p.post_type = 'fstn_bench' AND p.post_status = 'publish' ORDER BY d.bench_price DESC LIMIT 800, 20Plan: d: ref on object_lookup, about 5026 rows (Using index condition; Using where; Using filesort) → p: eq_ref on PRIMARY, about 1 row (Using where)
Filter on a column with no index
ACF, as generated by WP_Query
SELECT wp_posts.ID FROM wp_posts INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id ) WHERE 1=1 AND ( ( wp_postmeta.meta_key = 'bench_agent_email' AND wp_postmeta.meta_value = 'agent0@example.com' ) ) AND wp_posts.post_type = 'fstn_bench' AND ((wp_posts.post_status = 'publish')) GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC LIMIT 0, 20Plan: wp_postmeta: ref on meta_key, about 19034 rows (Using where; Using temporary; Using filesort) → wp_posts: eq_ref on PRIMARY, about 1 row (Using where)
Fieldstone
SELECT p.ID FROM wp_posts p INNER JOIN wp_fstn_data_bench_listing d ON d.object_id = p.ID AND d.object_type = 'post' WHERE p.post_type = 'fstn_bench' AND p.post_status = 'publish' AND d.bench_agent_email = 'agent0@example.com' ORDER BY p.post_date DESC LIMIT 20Plan: p: ref on type_status_date, about 5046 rows (Using where; Using index) → d: eq_ref on object_lookup, about 1 row (Using index condition; Using where)
Environment
| Hardware | Intel Core i5-9300H @ 2.40GHz, 8 GB RAM, SSD — a development laptop |
| Operating system | Windows NT 10.0 AMD64 |
| PHP | 8.2.4 (command line) |
| Database | MariaDB 10.4.28 |
| innodb_buffer_pool_size | 256.0 MB |
| innodb_flush_log_at_trx_commit | 1 |
| Query cache | OFF |
| WordPress | 7.1-RC1 |
| Object cache | none (in-process only) |
| ACF | 6.8.5 |
| Fieldstone | 1.3.1 |
| Date | 2026-10-02 |
What this does not tell you
- The Fieldstone queries are hand-written SQL. Fieldstone does not hook into
WP_Queryyet. Ameta_queryon a Fieldstone site still works, because of the postmeta mirror, but it runs againstwp_postmetaand gets none of the speed-up shown here. To get it you query the table yourself. - No persistent object cache. With Redis or Memcached, repeated reads of the same post come from the cache for both plugins and the read difference shrinks or disappears. Queries by field value are not helped by an object cache.
- One request at a time. This measures how long one operation takes, not how many a server handles under load.
- The command line, not a web page. No theme, no page rendering.
- Ten simple fields. No repeaters, relationships or galleries.
- One machine. Absolute times depend on hardware and database settings, and the ratios move too: more posts widen the indexed-query gap, fewer narrow it.
Run it yourself
The download contains the scripts, the method, and the raw results behind this page, including every individual timing. You need a throwaway WordPress install with ACF and Fieldstone both active, and WP-CLI.
php run.php --path=/path/to/wordpress --hardware="CPU, RAM, disk"
Download the benchmark code and raw data
If you get different results, we want to know: tell us and include the report it writes.
Try the storage on your own content
Fieldstone is free on WordPress.org, runs alongside ACF, and imports your ACF field groups in one click without touching your ACF data.
ACF is a trademark of its owner and is used here for identification only.