Why Your WooCommerce Discounted Products Don’t Show Up in the “On Sale” Shop Filter
WooCommerce Troubleshooting
The Filter That Finds Nothing
Your discount plugin is running. Products show sale prices. But the “On Sale” shop filter comes back empty โ and customers can’t find your deals. Here’s why.
You’ve set up a discount campaign. The product pages show crossed-out prices and sale badges. Customers can add to cart and see the discounted price at checkout. Everything looks like it’s working.
Then you check the “On Sale” filter in your shop โ or a customer mentions they can’t find your sale products through the filter โ and the results are empty. Your discounted products aren’t there.
This is one of the more confusing WooCommerce problems to hit, because the discount is clearly working on the product page. The issue isn’t that your plugin failed. It’s that your plugin and your filter are looking at different pieces of data. Understanding which piece each one reads is the key to diagnosing and fixing the problem.
Why the “On Sale” filter comes up empty
WooCommerce’s “On Sale” filter โ whether it’s a FacetWP facet, a JetSmartFilters filter, a YITH filter, or WooCommerce’s own built-in query โ works by asking a simple database question: which products have a sale price stored in the database right now?
That question is answered by a WooCommerce function called wc_get_product_ids_on_sale(). Internally, it queries a lookup table called wc_product_meta_lookup, checking a column named onsale. That column is set to 1 only when WooCommerce saves a product with a _sale_price value in the wp_postmeta table โ the actual database field for the product’s sale price.
If a product’s _sale_price field is empty, the onsale column is 0, and no filter will find it as being on sale โ regardless of what it displays on the product page.
The core mismatch
Filters and facets query the database for on-sale products. Some discount plugins modify prices at display time without ever writing to the database. These two approaches don’t talk to each other โ which is why filters come up empty even when the product page looks right.
Two discount architectures โ and why it matters
WooCommerce discount plugins generally fall into two architectural camps, and the “On Sale” filter problem belongs squarely to one of them.
Filter-based (on-the-fly) discounting
Most WooCommerce discount plugins โ including many popular rule-engine plugins โ work by hooking into WooCommerce’s pricing filters. When a product page or cart needs to display a price, WooCommerce fires a series of filter hooks: woocommerce_product_get_price, woocommerce_product_get_sale_price, woocommerce_get_price_html, and others. The plugin intercepts these, evaluates its rules, calculates the discount, and returns the modified price.
This happens entirely at request time, in PHP memory. Nothing is written to the database. The product record itself is unchanged โ the _sale_price field in wp_postmeta stays empty (or stays at whatever it was before). The discount only exists while a page is being rendered or a cart is being calculated.
The product page looks correct. Strikethrough pricing, sale badges, cart totals โ all work. But the database record says the product has no sale price. Any query that reads the database directly โ like wc_get_product_ids_on_sale() โ won’t find it.
Database-write discounting
The alternative approach is for a plugin to write the discounted price directly to the product’s _sale_price field in the database when a campaign activates, and clear it when the campaign ends. WooCommerce then reads that stored value like it reads any manually set sale price.
Because the sale price now exists in the database, the wc_product_meta_lookup table gets updated with onsale = 1, and wc_get_product_ids_on_sale() returns the product. Filters, facets, and any other tool that queries on-sale products will find it automatically.
This approach has tradeoffs of its own, which we’ll cover later. But on-sale filter compatibility is one area where it has a clear advantage.
How WooCommerce decides what’s “on sale” for filters
It helps to understand the specific chain of reads that underpins every “On Sale” shop filter in WooCommerce.
A filter or facet requests “on sale” products
FacetWP, JetSmartFilters, YITH filters, WooCommerce’s own “On Sale” product query โ all of these ultimately need a list of product IDs that are currently on sale.
Most use wc_get_product_ids_on_sale() or query the lookup table directly
WooCommerce’s canonical function for this retrieves a cached transient or queries wc_product_meta_lookup where onsale = 1. Many third-party filtering plugins call this function or replicate its logic in their own queries.
The lookup table reads from _sale_price postmeta
The onsale column in wc_product_meta_lookup is set when WooCommerce saves a product that has a non-empty _sale_price value in wp_postmeta and that _sale_price equals the current active _price. This only happens when a product is saved with a database-resident sale price.
Filter-based discounts never trigger a product save
A plugin that applies discounts via WooCommerce pricing filters modifies the price in memory at request time. It never saves the product, never writes to _sale_price, and never updates wc_product_meta_lookup. The lookup table has no knowledge of the discount.
The filter returns an empty result for those products
When the filter queries for on-sale products, the lookup table shows your discounted products as not on sale, and they’re excluded from the results. The filter is working correctly โ it just doesn’t know about the discount.
There’s also a caching layer to consider. wc_get_product_ids_on_sale() stores its result in a transient for up to 30 days. Even if you were to write the sale price to the database by another means, the cached result would be stale until the transient expires or is explicitly cleared.
Which plugins are affected
The on-sale filter problem affects any discount plugin that relies on WooCommerce pricing filter hooks rather than writing sale prices to the database. This is a large portion of the WooCommerce discount plugin ecosystem, including most rule-engine-style plugins.
The pattern to look for: if a plugin’s documentation talks about “real-time pricing,” “dynamic pricing rules evaluated at checkout,” or “cart-level discounts,” those phrases typically describe filter-based architecture. Prices are computed on demand, not stored in advance.
The issue is compounded in certain filter-based plugins by how they handle $product->is_on_sale(). That method calls get_sale_price() internally, which does fire WooCommerce’s pricing filters โ meaning is_on_sale() can return true for a product affected by a filter-based plugin, even though the database lookup table says otherwise. So on the product page, the product looks on sale (the method check passes). But the “On Sale” shop filter uses the lookup table, not per-product method calls, so it still excludes the product.
This distinction โ between what $product->is_on_sale() returns and what wc_get_product_ids_on_sale() returns โ is the core of the mismatch.
Not a bug in your filter plugin
If you open a support ticket with FacetWP or JetSmartFilters saying “On Sale filter doesn’t work,” they will likely tell you the filter is working correctly โ it’s looking at the right data. The root cause is upstream in how your discount plugin writes (or doesn’t write) its prices to the database.
How to diagnose your specific setup
Before assuming the problem or its cause, it’s worth confirming what’s actually happening. Here’s how to check.
Step 1: Check the product’s sale price in the database
Go to a product that your discount plugin has applied a discount to. Open the product edit page in WooCommerce admin and look at the Sale price field under Product Data โ General.
Is there a sale price value there? If the field is empty, your discount plugin is not writing to the database. The discount exists only at display time, and no on-sale filter will find this product โ regardless of what it looks like on the product page.
If there is a value in the sale price field, the plugin is writing to the database and the problem is likely elsewhere (probably a stale transient โ see below).
Step 2: Check the WooCommerce on-sale transient
Even when a product has a database-resident sale price, the wc_products_onsale transient may be stale. This transient caches the result of the on-sale product lookup for up to 30 days. If a sale price was added or removed since the transient was set, the filter results won’t reflect the current state until the transient is cleared.
To clear it: go to WooCommerce โ Status โ Tools and run “Clear transients.” This forces WooCommerce to rebuild the on-sale product list from current database state on the next filter query.
Step 3: Check how your filter plugin resolves on-sale products
Different filtering plugins have different approaches. Some call wc_get_product_ids_on_sale() directly. Some query the wc_product_meta_lookup table directly. A few call $product->is_on_sale() per product. The last approach will return the correct result for filter-based discount plugins, because it calls get_sale_price() which fires the discount filter โ but it’s also slower for large catalogs.
Check your specific filtering plugin’s documentation for how its “On Sale” option works. FacetWP’s documentation, for example, explains how it resolves product queries, and whether the on-sale option uses wc_get_product_ids_on_sale() or a per-product check. That determines whether a filter-based discount plugin can work with it at all without workarounds.
Quick database check
If you have phpMyAdmin or WP-CLI access, you can check the lookup table directly: SELECT product_id, onsale FROM wp_wc_product_meta_lookup WHERE onsale = 1; If a product you expect to be on sale isn’t in those results, the database-resident sale price is missing.
Workarounds short of switching plugins
If you’re not in a position to switch discount plugins, there are some approaches that can partially address the problem โ with different tradeoffs.
Apply manual sale prices alongside the plugin discount
Some stores set a manual sale price on each product in WooCommerce admin (to populate the _sale_price field and trigger the lookup table) and then let the discount plugin add its own modifier on top. This is fragile โ you’re managing two separate price layers and they can drift out of sync. But for a small number of products and a simple discount structure, it can work as a stopgap.
Use a custom hook to write the sale price
A developer can write a small WordPress plugin or functions.php snippet that hooks into campaign activation events (if your discount plugin exposes them) and writes the discounted price to the product’s _sale_price field. After saving, you’d also need to trigger a WooCommerce product save to update the lookup table, and clear the wc_products_onsale transient.
This is the most technically sound workaround, but it requires custom code and ongoing maintenance. If your discount plugin changes how it stores or applies discounts, the custom hook may break silently.
Build a custom “On Sale” facet using a different data source
FacetWP and some other filtering plugins allow custom data sources for facets. You could build a facet based on a custom product meta field that your discount plugin writes (if it writes any product-level metadata), or manually maintained taxonomy. This is a significant custom development effort, but it bypasses the wc_get_product_ids_on_sale() problem entirely by using a different data source.
Use URL parameters to pre-filter to sale products
As a purely front-end workaround, some stores create a “Shop Sale” page or link that uses URL query parameters to filter products โ either by a custom meta value or by linking directly to a category page that shows only products currently on sale. This doesn’t fix the facet filter, but it gives customers a way to browse sale items that doesn’t depend on the broken on-sale query.
The real fix: understanding what to look for in a discount plugin
If on-sale filter compatibility matters for your store โ and for stores with active filtering setups it usually does โ the architectural question is worth asking before choosing a discount plugin, not after.
The core question: does this plugin write discounts to WooCommerce’s native _sale_price field when a campaign activates, and clear that field when the campaign ends?
A plugin that does this will:
- Populate the
wc_product_meta_lookup.onsalecolumn correctly - Make discounted products appear in all standard “On Sale” shop queries
- Work with FacetWP, JetSmartFilters, YITH filtering, and WooCommerce’s own on-sale product blocks automatically
- Show strikethrough pricing through WooCommerce’s native display system, which all themes understand
A plugin that does this also inherits the classic tradeoffs of the database-write approach: activating a discount requires saving price changes to potentially many products, which takes time and creates a gap between campaign start and all products being updated. The same is true at campaign end. For large catalogs, this can be a meaningful consideration. It’s the right tradeoff to understand before choosing.
For a broader look at how these architectural choices affect store performance as well, the post on WooCommerce discount plugin performance goes into detail on rule-engine vs. campaign-based approaches and what Query Monitor shows for each.
Questions to ask when evaluating a plugin
When reading a discount plugin’s documentation or readme, look for explicit answers to:
- Does the plugin write to WooCommerce’s native
_sale_pricepostmeta field on campaign activation? - Does the plugin update the product on deactivation/expiry โ restoring original prices and clearing the sale price field?
- Do the plugin’s own docs or FAQ mention on-sale filter compatibility specifically, and which filtering plugins are tested?
If the documentation doesn’t address this, it’s worth testing specifically. Create a campaign, let it activate, then query wc_get_product_ids_on_sale() (or check the product’s sale price field in admin). That tells you definitively whether the plugin uses database-write or filter-only architecture for on-sale status.
This is one of the dimensions explored in the broader comparison of WooCommerce discount plugins โ specifically whether each plugin’s discounts surface correctly in native WooCommerce queries. It’s a meaningful differentiator that doesn’t always show up in the feature lists.
The honest tradeoff
There’s no perfect architecture here. Filter-based plugins are lighter โ they don’t need to save product records on activation, so activating a campaign across 500 products is nearly instant. Database-write plugins are heavier at activation time but create full compatibility with every tool that reads WooCommerce’s standard on-sale data.
If your store has an active “On Sale” filter that customers use to browse sale products, and if that filter is important to your conversion flow, the database-write approach is the one that works without workarounds. If your store doesn’t use an on-sale shop filter, the architectural difference is less important, and a filter-based plugin may be perfectly adequate.
Understanding this tradeoff before installing a plugin saves you the frustration of discovering it after customers start asking why they can’t find your sale products. It’s also covered in the post on WooCommerce dynamic pricing, which explains the different discount mechanisms and when each one fits.
Frequently asked questions
Why are my WooCommerce on sale products not showing in the On Sale filter?
The most common cause is that your discount plugin applies discounts at display time via WooCommerce pricing filter hooks, rather than writing the discounted price to the product’s _sale_price database field. WooCommerce’s “On Sale” filter infrastructure โ including wc_get_product_ids_on_sale() and the wc_product_meta_lookup.onsale column โ reads from the database, not from the result of pricing filters. If the field is empty, the filter won’t find the product.
Does clearing the WooCommerce transients fix the On Sale filter?
Sometimes, but not always. Clearing transients (WooCommerce โ Status โ Tools โ Clear transients) forces WooCommerce to rebuild the on-sale product list from current database state. If the underlying issue is a stale cache and the database does have a correct _sale_price set, this fixes it. But if the database field is empty โ because your discount plugin only applies prices via filters โ clearing transients won’t change the result. The filter will still return empty for those products.
Will the FacetWP “On Sale” facet work with filter-based discount plugins?
FacetWP’s On Sale facet option resolves products using WooCommerce’s on-sale query infrastructure, which reads from the wc_product_meta_lookup table. Filter-based discount plugins don’t write to that table, so products discounted via filters won’t appear in FacetWP’s On Sale facet. The same applies to JetSmartFilters and YITH filtering plugins that use the same WooCommerce on-sale query. Check the specific plugin’s documentation for how it resolves on-sale products โ some filtering plugins offer alternative data source configurations.
Does a discount plugin that writes to _sale_price slow down my store?
At activation and deactivation, yes โ writing or clearing the sale price for many products requires saving each product record, which takes time. For a campaign covering 500 products, this is a meaningful operation. During the campaign’s active period, however, the performance picture is the reverse: because the discounted price is already stored in the database, product pages and shop pages don’t need to run any additional computations to resolve the price. The discount is just there in the data. This is a genuine tradeoff between activation cost and runtime performance.
What’s the difference between $product->is_on_sale() and wc_get_product_ids_on_sale()?
$product->is_on_sale() is a per-product method call that compares the product’s sale price to its regular price using WooCommerce’s data access layer. Because it calls get_sale_price(), which fires WooCommerce’s pricing filters, it can return true for products discounted by a filter-based plugin. wc_get_product_ids_on_sale() is a catalog-level query that reads the wc_product_meta_lookup table from the database in bulk. It does not fire per-product filters. These two can disagree โ a product can be “on sale” according to is_on_sale() but not present in the result of wc_get_product_ids_on_sale(). Most “On Sale” shop filters use the catalog query, not per-product checks.
How do I check if my discount plugin writes to the _sale_price field?
The simplest check: install your discount plugin, create a campaign that discounts a specific product, activate the campaign, then open that product in WooCommerce admin โ Product Data โ General โ Sale price. If there’s a value in the field, the plugin is writing to the database. If the field is empty but the product page still shows a discount, the plugin is using filter-based on-the-fly pricing. You can also query the database directly: SELECT meta_value FROM wp_postmeta WHERE post_id = {PRODUCT_ID} AND meta_key = '_sale_price';
Can I use a custom snippet to fix the On Sale filter without switching plugins?
Yes, in principle. A developer can write code that hooks into your discount plugin’s activation events, reads the calculated discounted price, writes it to the product’s _sale_price postmeta, saves the product (to update the lookup table), and clears the wc_products_onsale transient. The reverse on deactivation. This requires understanding your specific plugin’s hook system, PHP development, and ongoing maintenance when the plugin updates. It’s workable but fragile โ if the plugin changes how it fires activation events, the fix breaks silently.
Key Takeaways
- WooCommerce “On Sale” filters read from the
wc_product_meta_lookup.onsaledatabase column, which only reflects products with a non-empty_sale_priceinwp_postmeta - Filter-based discount plugins apply prices at display time via WooCommerce hooks โ they never write to
_sale_price, so the lookup table stays empty for their discounted products - FacetWP, JetSmartFilters, YITH filters, and WooCommerce’s own on-sale queries all use this database-level lookup โ making them incompatible with filter-only discount plugins out of the box
$product->is_on_sale()andwc_get_product_ids_on_sale()can disagree: the first fires pricing filters, the second reads the database directly. Shop filters use the database query- Clearing the WooCommerce transient only helps if the database has a correct
_sale_pricestored โ if the field is empty, clearing transients changes nothing - The architectural tradeoff: database-write plugins are slower to activate but give full on-sale filter compatibility; filter-based plugins activate instantly but are invisible to on-sale shop queries
- Before choosing a discount plugin, check explicitly whether it writes to
_sale_priceon activation โ test by inspecting the product edit page after a campaign activates