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.

17.6× faster
Filtering posts by one indexed field
18.2 ms on ACF, 1.03 ms on Fieldstone
3.3× faster
Reading 10 fields of one post
0.77 ms on ACF, 0.23 ms on Fieldstone
1.6× slower
Saving 10 changed fields, default settings
8.33 ms on ACF, 13.7 ms on Fieldstone

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.

Fieldstone is ours, so read this the way you would read any vendor benchmark. The tests that make Fieldstone look bad are included, the limits are listed below, and you can run the same code on your own server.

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.

Filter by one field city = X, newest 2017.6× faster
ACF18.2 ms
Fieldstone1.03 ms
Numeric range price between A and B, newest 2010.6× faster
ACF20.8 ms
Fieldstone1.97 ms
Three filters + total count city, bedrooms ≥ 3, featured, newest 20374× faster
ACF465 ms
Fieldstone1.24 ms
Sort by a field, first pages price, pages 1–587.2× faster
ACF36.0 ms
Fieldstone0.41 ms
Sort by a field, deep pages price, page 41 onwards1.7× faster
ACF36.7 ms
Fieldstone21.2 ms
Filter on a column with no index agent email = X, newest 201.3× faster
ACF18.8 ms
Fieldstone14.7 ms

Reading values

Time through each plugin’s own functions: get_field() and fstn_get_field().

Read 10 fields of one post cold cache3.3× faster
ACF0.77 ms
Fieldstone0.23 ms
Archive page query 20 posts, read 10 fields of each1.9× faster
ACF12.3 ms
Fieldstone6.54 ms

Saving values

Time through each plugin’s own functions: update_field() and fstn_update_field().

Save 10 changed fields on an existing post1.6× slower
ACF8.33 ms
Fieldstone13.7 ms
First save of 10 fields on a new post1.1× faster
ACF15.6 ms
Fieldstone13.8 ms

How to read these

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

TestACFFieldstoneFieldstone, mirror offFieldstone 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

TestACFFieldstoneFieldstone, mirror offFieldstone 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

TestACFFieldstoneFieldstone, mirror offFieldstone 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 rowspostmeta sizeTable rowsTable sizeTotal
ACF200,00027.6 MB––27.6 MB
Fieldstone100,00014.5 MB10,0005.0 MB19.6 MB
Fieldstone, mirror off00.0 MB10,0005.0 MB5.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.

  1. 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.
  2. Written the normal way. Values go in through update_field() and fstn_update_field(), then a sample is read back and compared with what was written.
  3. 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.
  4. Warm-up, then 20 timed runs. 3 runs are thrown away first. Runs rotate through different filter values and pages.
  5. 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, 20

Plan: 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 20

Plan: 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, 20

Plan: 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 20

Plan: 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, 20

Plan: 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 20

Plan: 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, 20

Plan: 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, 20

Plan: 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, 20

Plan: 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, 20

Plan: 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, 20

Plan: 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 20

Plan: 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

HardwareIntel Core i5-9300H @ 2.40GHz, 8 GB RAM, SSD — a development laptop
Operating systemWindows NT 10.0 AMD64
PHP8.2.4 (command line)
DatabaseMariaDB 10.4.28
innodb_buffer_pool_size256.0 MB
innodb_flush_log_at_trx_commit1
Query cacheOFF
WordPress7.1-RC1
Object cachenone (in-process only)
ACF6.8.5
Fieldstone1.3.1
Date2026-10-02

What this does not tell you

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.

Install Fieldstone free

ACF is a trademark of its owner and is used here for identification only.

← Fieldstone vs ACF Migrate from ACF →