← All posts

Why most WordPress directory plugins stop scaling past 5,000 listings

Faceted search built on meta_query degrades the moment your listing count crosses 5,000. Here's the architecture decision Listora makes instead: a dedicated search index table.

The 5,000-listing wall

Directory plugins start fast. The first 200 listings render quickly because WordPress is fundamentally a CMS - reading 200 posts with a few meta values is what it’s good at. Search works. Filters work. The site feels snappy.

Then the listing count grows. By 2,000 listings, search starts feeling slow. By 5,000, the faceted filters take 3-4 seconds to return. By 10,000, the directory is unusable for any query more specific than “all listings.”

The cause is almost always the same: the plugin is running its search through meta_query.

Why meta_query doesn’t scale

meta_query joins the wp_posts table against wp_postmeta once per filter. A query like:

Restaurants in Austin with rating >= 4 and price = $$ and open_now = true

runs four JOINs against postmeta. Each JOIN is N rows where N is the total meta-value count across all listings. At 5,000 listings with 30 meta keys each, that’s 150,000 rows joined four times. Even with indexes, the planner has to scan through them.

WordPress was designed for content sites where meta is read once per post-fetch, not for directory sites where meta is the filter axis.

The fix: a denormalized search index

The correct architecture for a directory at scale is a dedicated, denormalized search-index table. One row per listing, every searchable + filterable attribute as a column (or a FULLTEXT-indexed text field), built at the time the listing is created or edited.

A search query then runs against a single table:

SELECT listing_id FROM listora_search_index
WHERE listing_type = 'restaurant'
  AND status = 'publish'
  AND city = 'Austin'
  AND avg_rating >= 4
ORDER BY is_featured DESC, avg_rating DESC
LIMIT 20;

The common filters (type, status, rating, price, featured, city, coordinates) are real indexed columns, so they need no postmeta JOINs. Indexes do the heavy lifting.

What Listora does differently

Listora ships a dedicated listora_search_index table that’s populated by an indexer that runs:

  • On listing create (the listing is searchable the moment it is saved)
  • On listing edit
  • On a full rebuild, which runs in background batches so it does not block the admin
  • On demand via wp listora reindex, or Settings - Advanced - Rebuild search index

Custom-field filters (cuisine, bedrooms, salary and so on) go through a second table, listora_field_index, which stores one indexed row per field value instead of a postmeta JOIN per filter.

Geo queries run against a separate listora_geo table indexed on (lat, lng). A bounding box on that index trims the candidate set first, then Haversine distance is computed in SQL on what is left.

Full-text search uses MySQL’s native FULLTEXT index on the search-index table’s title, content and meta text columns.

The result is a search path that reads dedicated index tables instead of stacking meta_query JOINs. No premium infrastructure required.

Why this matters for your business case

A directory plugin that hits a 5,000-listing wall is a business problem, not a technical one. You can’t run paid acquisition that targets growth beyond 5,000 listings if your search times out. You can’t pitch the directory as a Yelp competitor if Yelp loads in 200ms and yours takes 4 seconds.

The architecture decision happens once, at install. Picking a plugin that ships with meta_query-based search is a decision that limits your business to the first 5,000 listings. Picking one with a denormalized index removes that ceiling.

Action Scheduler for the rest

Search is the obvious scaling bottleneck. The less-obvious ones are background jobs:

  • Sending listing-expiration emails
  • Reindexing on bulk updates
  • Cron-driven cleanup
  • Expiring paid plans and renewals

WordPress’s built-in WP-Cron is request-driven. On a busy site, it fires erratically. On a low-traffic site, it doesn’t fire at all. On a high-traffic site, it fires too often and slows down page requests.

Listora bundles WooCommerce’s Action Scheduler library. Recurring jobs prefer Action Scheduler, with a WP-Cron fallback when it is not available. Jobs queue, get logged, and can be retried. It is the same library WooCommerce stores rely on.

This is the kind of decision that doesn’t matter on day one and matters every day after.

TL;DR

If you’re picking a directory plugin and the listing count will plausibly grow past 5,000, the only question that matters is: does the search run on meta_query or on a dedicated index table? Test it: import a large CSV of listings and run a few facet queries. The plugin that holds up is the one to build on.

Listora was designed around this exact constraint from day one. The search index is in the free plugin, not gated behind a Pro tier.

Try it: download Listora free and run wp listora demo seed --pack=all to load about 128 listings. Then import a few thousand of your own with wp listora import and watch the facet queries.