Every other WordPress custom fields plugin — Advanced Custom Fields (ACF), Secure Custom Fields (SCF), Meta Box, Pods, CMB2, Carbon Fields — stores field values in wp_postmeta. For small sites this works fine. For sites with 1,000+ posts, complex repeaters, or frequent frontend queries, it becomes the performance bottleneck that makes WordPress slow.
Field Forge uses a dedicated indexed table for field values. Combined with batch loading and object cache integration, this delivers 3–10x faster query times on real-world sites.
WordPress’s wp_postmeta table is a generic key-value store attached to every post. It’s how post_title vs post_content vs custom_field_x all get stored. The schema looks like this:
CREATE TABLE wp_postmeta (
meta_id BIGINT(20) PRIMARY KEY,
post_id BIGINT(20),
meta_key VARCHAR(255),
meta_value LONGTEXT,
INDEX (post_id),
INDEX (meta_key(191))
);
Simple. Flexible. Works for any key-value pair. And on a typical plugin-heavy WordPress site, it’s enormous — easily millions of rows on a 5-year-old e-commerce site with lots of orders and product meta.
Here’s what happens when you load an archive page with 20 posts, each with 10 custom fields:
-- Query 1: Fetch posts
SELECT * FROM wp_posts WHERE post_type = 'post' LIMIT 20;
-- Queries 2-201: For each post, fetch each field
SELECT meta_value FROM wp_postmeta WHERE post_id = 1 AND meta_key = 'field_a';
SELECT meta_value FROM wp_postmeta WHERE post_id = 1 AND meta_key = 'field_b';
-- ... 198 more queries
200 separate queries. Each query is fast individually, but the round-trip overhead adds up. On sites hitting the database over a network (common with managed hosting), this can add 500ms+ to every pageview. Cumulative Layout Shift goes up. Core Web Vitals drop. SEO suffers.
Repeater fields in ACF are stored with one meta row per sub-field per row. A repeater with 5 sub-fields and 10 rows creates 50 meta entries per post. Add 20 posts to an archive and you’re looking at 1,000+ meta queries on a single pageview. This is why ACF-heavy sites notoriously slow down past a certain scale.
Field Forge stores field values in a dedicated wp_fieldforge_values table:
CREATE TABLE wp_fieldforge_values (
id BIGINT(20) PRIMARY KEY,
post_id BIGINT(20) NOT NULL,
field_group_id BIGINT(20) NOT NULL,
field_name VARCHAR(255) NOT NULL,
parent_id BIGINT(20) NULL, -- for nested values (repeater/group/flex)
row_index INT NULL, -- for repeater row position
value LONGTEXT,
INDEX (post_id, field_name),
INDEX (post_id, field_group_id),
INDEX (parent_id, row_index)
);
Key differences from wp_postmeta:
parent_id, row_index) — nested values (repeater, group, flex) are structured, not string-concatenatedwp_postmeta don’t pollute field datawp_postmeta isn’tWe ran controlled benchmarks on a test site with 10,000 posts, each with a field group containing 15 fields (3 text, 2 WYSIWYG, 1 image, 1 repeater with 5 sub-fields, 1 flexible content with 3 layout types).
| wp_postmeta (ACF / SCF) | Field Forge custom table | |
|---|---|---|
| SQL queries | 302 | 1 (batch_load) |
| Query time (local DB) | 840ms | 95ms |
| Query time (network DB, +20ms RTT) | 6,880ms | 115ms |
| First Contentful Paint | 2,100ms | 340ms |
| wp_postmeta | Field Forge | |
|---|---|---|
| SQL queries | 16 | 1 |
| Query time (local DB) | 45ms | 12ms |
| Query time (network DB, +20ms RTT) | 365ms | 32ms |
| wp_postmeta | Field Forge | |
|---|---|---|
| SQL queries | 1,000 INSERTs | 1 multi-row INSERT |
| Time (local DB) | 1,240ms | 48ms |
| Time (network DB) | 21,240ms | 68ms |
On network-attached databases (which is how nearly all managed WordPress hosting works — Kinsta, WP Engine, Cloudways, SiteGround Cloud, etc.), the difference is dramatic. Round-trip latency is the dominant cost, and Field Forge makes fewer round trips.
Developer-facing API for explicit batch loading when Field Forge’s automatic preloading isn’t enough:
// Load fields for a specific set of posts in one query
$post_ids = [1, 2, 3, 4, 5];
FieldForge::batch_load($post_ids);
// Now any get_field() call on these posts hits the cache
foreach ($post_ids as $id) {
$hero = get_field('hero_title', $id); // No DB query
}
The batch_load() call issues a single WHERE post_id IN (...) query and populates Field Forge’s in-memory cache for all requested posts. Subsequent get_field() calls are cache hits.
Field Forge hooks into WordPress’s the_posts filter to auto-preload fields for the main query. Archive pages, search results, and category listings get batch-loaded field data automatically — no code changes required.
Field Forge respects WordPress’s wp_cache_* API:
fieldforge) so it doesn’t conflict with other plugins or core cache operations.Cache invalidation happens automatically on field value update or delete via action hooks.
Faster archive pages, faster search results, faster dynamic templates. Core Web Vitals improve. Client retention improves because the site “feels snappy.”
Product listing pages with 30+ products and complex custom fields render in the same time as simpler sites. Shopping cart interactions are faster because product meta loads are batched.
REST API and WPGraphQL responses are faster. Static site generators (Next.js ISR, Astro, Nuxt) hit fewer database queries per build. Build times improve.
Field Forge’s ACF compatibility layer means get_field() calls in your theme return the same values — but they come from the custom table, not wp_postmeta. The performance improvement happens transparently after migration.
To be fair: if your site has under 500 posts and simple custom fields (no deep repeaters, no flexible content), the wp_postmeta approach works. You won’t notice a speed difference. Field Forge’s performance advantage becomes meaningful at scale — 1,000+ posts, complex repeaters, or sites on network-attached databases.
Get Field Forge — from $35/year →
Custom table storage is included in every version of Field Forge, including the free one.