You’re selling products with expiration dates – food items, cosmetics, event tickets, seasonal merchandise – but WooCommerce doesn’t track when they expire.
Table of Contents
- Add Expiry Date Field to Products (45 minutes)
- Automatically Hide Expired Products (30 minutes)
- Show ‘Expires Soon’ Warnings to Customers (20 minutes)
- Bulk Update Expiry Dates (15 minutes)
- Send Email Alerts Before Products Expire (Advanced)
- Handle Variable Products with Different Expiry Dates
- What About Batch/Lot Tracking?
- Plugin Alternatives (Quick Overview)
- Conclusion
Customers complain about receiving near-expired products, you’re manually checking inventory spreadsheets, and you’ve probably sold expired stock without realizing it.
Here’s the reality: WooCommerce has no built-in expiry date management. But adding it yourself isn’t complicated, and you don’t necessarily need a plugin to get started.
This guide walks you through everything from adding a basic expiry date field to full automation that hides expired products and sends alerts. Select what you need – most stores only use 2-3 of these features.
Add Expiry Date Field to Products (45 minutes)
Start with the basics – adding a custom date field to your product data.
This field stores the expiry date and becomes the foundation for everything else: automation, warnings, filtering.
Here’s the code approach using WooCommerce’s product data hooks:
// Add this to your child theme's functions.php or custom plugin
add_action( 'woocommerce_product_options_general_product_data', 'add_expiry_date_field' );
function add_expiry_date_field() {
woocommerce_wp_text_input( array(
'id' => '_expiry_date',
'label' => __( 'Expiry Date', 'woocommerce' ),
'placeholder' => 'YYYY-MM-DD',
'desc_tip' => true,
'description' => __( 'Enter the product expiry date', 'woocommerce' ),
'type' => 'date',
) );
}
This hooks into WooCommerce’s product editing screen and adds a date picker field in the General tab.
The woocommerce_wp_text_input function handles the HTML output and integrates with WooCommerce’s UI styling automatically.
Now save the field data when the product is updated:
add_action( 'woocommerce_process_product_meta', 'save_expiry_date_field' );
function save_expiry_date_field( $post_id ) {
$expiry_date = isset( $_POST['_expiry_date'] ) ? sanitize_text_field( $_POST['_expiry_date'] ) : '';
update_post_meta( $post_id, '_expiry_date', $expiry_date );
}
This saves the expiry date to the product’s metadata using WordPress’s post meta system.
The underscore prefix (_expiry_date) keeps it hidden from WooCommerce’s default custom fields display.
Tip: The
type => 'date'parameter triggers WordPress’s built-in date picker, giving you a calendar interface instead of manual date entry. This prevents formatting errors and makes data entry faster.
Display Expiry Date on Product Pages
Customers need to see expiry dates before purchasing. Here’s how to show it on your product pages:
add_action( 'woocommerce_single_product_summary', 'display_expiry_date', 25 );
function display_expiry_date() {
global $product;
$expiry_date = get_post_meta( $product->get_id(), '_expiry_date', true );
if ( ! empty( $expiry_date ) ) {
$formatted_date = date( 'F j, Y', strtotime( $expiry_date ) );
echo '<p class="expiry-date"><strong>Expires:</strong> ' . esc_html( $formatted_date ) . '</p>';
}
}
This hooks into the product summary (where price and add-to-cart button appear) and displays the expiry date if one exists.
The priority 25 places it after the price but before the add-to-cart button. Adjust the number to change placement.
The strtotime and date functions convert your stored YYYY-MM-DD format into something readable like “March 15, 2026.”
Only products with expiry dates set will show anything – empty fields stay hidden.
Automatically Hide Expired Products (30 minutes)
Manual checking doesn’t scale. Once you’re tracking 50+ products with expiry dates, you need automation to keep expired items out of your store.
You have two approaches: query filtering (real-time checks) or scheduled status updates (cron-based). Query filtering is simpler to start with.
add_action( 'pre_get_posts', 'hide_expired_products' );
function hide_expired_products( $query ) {
// Only run on main WooCommerce product queries
if ( ! is_admin() && $query->is_main_query() && ( is_shop() || is_product_category() || is_product_tag() ) ) {
$meta_query = $query->get( 'meta_query' ) ?: array();
// Exclude products where expiry date exists and is in the past
$meta_query[] = array(
'relation' => 'OR',
array(
'key' => '_expiry_date',
'compare' => 'NOT EXISTS', // Include products without expiry dates
),
array(
'key' => '_expiry_date',
'value' => date( 'Y-m-d' ),
'compare' => '>=',
'type' => 'DATE',
),
);
$query->set( 'meta_query', $meta_query );
}
}
This modifies WooCommerce’s product queries before they run, excluding expired products from shop pages, categories, and tag archives.
The logic says “show products with no expiry date OR products where expiry date is today or later.”
The pre_get_posts filter runs on every query, but we limit it to main queries on WooCommerce pages only. Running this on admin queries would break your backend product list.
Warning: This is real-time checking, meaning WordPress runs the comparison on every page load. For stores with thousands of products, this adds query overhead. The alternative is scheduled status updates via cron, which checks once daily and marks products as out-of-stock instead of filtering queries.
Schedule Automatic Status Updates with Cron
For better performance on large stores, run a daily check that updates product status instead of filtering queries:
// Schedule daily expiry check
add_action( 'wp', 'schedule_expiry_check' );
function schedule_expiry_check() {
if ( ! wp_next_scheduled( 'check_expired_products' ) ) {
wp_schedule_event( time(), 'daily', 'check_expired_products' );
}
}
// Run the expiry check
add_action( 'check_expired_products', 'update_expired_product_status' );
function update_expired_product_status() {
$args = array(
'post_type' => 'product',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => '_expiry_date',
'value' => date( 'Y-m-d' ),
'compare' => '<',
'type' => 'DATE',
),
),
);
$expired_products = new WP_Query( $args );
if ( $expired_products->have_posts() ) {
while ( $expired_products->have_posts() ) {
$expired_products->the_post();
$product = wc_get_product( get_the_ID() );
$product->set_stock_status( 'outofstock' );
$product->save();
}
}
wp_reset_postdata();
}
This creates a WordPress cron job that runs once daily, finds all products with expiry dates in the past, and marks them as out-of-stock.
Products stay in your catalog but can’t be purchased.
The benefit? One query per day instead of filtering on every page load.
The downside? Products don’t hide immediately at midnight – they hide the next time the cron runs.
Remember: Expired products should stay in your order history and customer purchase records. Only hide them from new purchases. Never delete expired products – you need that data for reporting, refunds, and customer service.
Show ‘Expires Soon’ Warnings to Customers (20 minutes)
Transparency builds trust. Customers appreciate knowing a product expires soon – it reduces returns and complaints about receiving near-expired items.
Define your “soon” threshold first. Seven days works for most products, but adjust based on your inventory: dairy might need 3 days, supplements might be fine with 30 days.
add_action( 'woocommerce_single_product_summary', 'show_expires_soon_warning', 26 );
function show_expires_soon_warning() {
global $product;
$expiry_date = get_post_meta( $product->get_id(), '_expiry_date', true );
if ( ! empty( $expiry_date ) ) {
$days_until_expiry = floor( ( strtotime( $expiry_date ) - time() ) / ( 60 * 60 * 24 ) );
if ( $days_until_expiry <= 7 && $days_until_expiry > 0 ) {
echo '<p class="expires-soon-warning">
<strong>Note:</strong> This product expires in ' . $days_until_expiry . ' days.
</p>';
} elseif ( $days_until_expiry <= 0 ) {
echo '<p class="expired-warning">
<strong>Warning:</strong> This product has expired.
</p>';
}
}
}
This calculates days remaining until expiry and shows a warning for products within 7 days.
The inline styles add visual weight – yellow for "expires soon," red for "expired" (though expired products should already be hidden if you implemented the previous section).
Tip: Use color psychology effectively. Yellow/orange signals caution without panic. Red signals urgency or problems. Green can indicate "fresh" or "plenty of time remaining" if you want to highlight recently stocked items.
Cart Page Expiry Warnings
Warning customers at the product page is good. Warning them in the cart before checkout is better:
add_action( 'woocommerce_check_cart_items', 'warn_about_expiring_cart_items' );
function warn_about_expiring_cart_items() {
foreach ( WC()->cart->get_cart() as $cart_item ) {
$product = $cart_item['data'];
$expiry_date = get_post_meta( $product->get_id(), '_expiry_date', true );
if ( ! empty( $expiry_date ) ) {
$days_until_expiry = floor( ( strtotime( $expiry_date ) - time() ) / ( 60 * 60 * 24 ) );
if ( $days_until_expiry <= 7 && $days_until_expiry > 0 ) {
wc_add_notice(
sprintf(
__( 'Note: %s expires in %d days.', 'woocommerce' ),
$product->get_name(),
$days_until_expiry
),
'notice'
);
}
}
}
}
This runs on cart page load and checkout, showing notices for any items expiring soon.
WooCommerce's notice system displays these prominently without blocking checkout – customers can still purchase, but they're informed.
If you're discounting products nearing expiry to clear inventory faster, check how those discounts affect your profit margin before setting prices. Running 30% off on items that already have thin margins might cost you money.
Bulk Update Expiry Dates (15 minutes)
Adding expiry dates one-by-one through the product editor doesn't scale.
You receive a shipment of 200 items with the same expiry date – you need bulk updates.
WooCommerce's CSV importer handles this cleanly. Export your products, add an expiry date column, and reimport.
Here's the process:
- Go to WooCommerce → Products → Export
- Export your products to CSV
- Open in a spreadsheet (Excel, Google Sheets, LibreOffice)
- Add a column header:
meta:_expiry_date - Fill in expiry dates using YYYY-MM-DD format
- Save as CSV
- Go to WooCommerce → Products → Import
- Upload your CSV and map columns (WooCommerce should auto-map the meta field)
The meta: prefix tells WooCommerce this is custom metadata, and the underscore prefix matches your custom field name.
Custom Bulk Editor (Optional Advanced)
WooCommerce's bulk editor doesn't include custom fields by default.
Adding bulk edit capability requires modifying admin screens and processing bulk actions – it's doable but complex enough that a plugin might be the better choice here.
If you need frequent bulk updates beyond CSV imports, this is one area where a dedicated expiry management plugin saves time compared to custom code.
Send Email Alerts Before Products Expire (Advanced)
Automated alerts help you take action before products expire: discount them, remove them from sale, reorder inventory, or transfer stock between locations.
There are two alert types: admin alerts (notify store managers) and customer alerts (notify buyers of products they purchased).
Admin alerts are more common and simpler to implement.
Daily Check for Expiring Products
Set up a cron job that checks for products expiring in the next 7 days and emails you the list:
// Schedule daily expiry alert check
add_action( 'wp', 'schedule_expiry_alerts' );
function schedule_expiry_alerts() {
if ( ! wp_next_scheduled( 'send_expiry_alerts' ) ) {
wp_schedule_event( strtotime( '08:00:00' ), 'daily', 'send_expiry_alerts' );
}
}
add_action( 'send_expiry_alerts', 'send_expiring_products_email' );
function send_expiring_products_email() {
$alert_threshold = 7; // days
$check_date = date( 'Y-m-d', strtotime( "+{$alert_threshold} days" ) );
$args = array(
'post_type' => 'product',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => '_expiry_date',
'value' => array( date( 'Y-m-d' ), $check_date ),
'compare' => 'BETWEEN',
'type' => 'DATE',
),
),
);
$expiring_products = new WP_Query( $args );
if ( $expiring_products->have_posts() ) {
$email_content = "Products expiring in the next {$alert_threshold} days:\n\n";
while ( $expiring_products->have_posts() ) {
$expiring_products->the_post();
$product = wc_get_product( get_the_ID() );
$expiry_date = get_post_meta( get_the_ID(), '_expiry_date', true );
$stock = $product->get_stock_quantity();
$email_content .= sprintf(
"- %s (Expires: %s, Stock: %s)\n Edit: %s\n\n",
get_the_title(),
date( 'F j, Y', strtotime( $expiry_date ) ),
$stock ?: 'N/A',
admin_url( 'post.php?post=' . get_the_ID() . '&action=edit' )
);
}
wp_mail(
get_option( 'admin_email' ),
'WooCommerce: Products Expiring Soon',
$email_content
);
}
wp_reset_postdata();
}
This finds products expiring within 7 days and sends a plain text email to your admin email address.
The email includes product names, expiry dates, stock levels, and direct edit links.
The strtotime( '08:00:00' ) schedules the cron to run at 8 AM daily. Adjust to match your working hours.
Email Template
Keep admin alert emails simple. You need product name, expiry date, stock level, and a link to take action.
Fancy HTML templates slow you down – you're reading this email to make decisions, not admire design.
For multiple alert thresholds (30 days, 7 days, 1 day), duplicate the cron job with different threshold values and email subjects.
This lets you set up workflows: 30-day alert triggers, discount campaigns, 7-day alert triggers, stock checks, 1-day alert triggers, and emergency removal.
Tip: Set up multiple alert thresholds (30 days, 7 days, 1 day) for different actions. Use 30 days to plan discount campaigns, 7 days to verify stock accuracy, and 1 day for emergency removal or deep discounting.
Handle Variable Products with Different Expiry Dates
Variable products add complexity. Same product, different sizes or colors, but potentially different expiry dates depending on when each variation was stocked.
The solution: add expiry dates at the variation level instead of the parent product.
// Add expiry date to variations
add_action( 'woocommerce_variation_options_pricing', 'add_variation_expiry_date', 10, 3 );
function add_variation_expiry_date( $loop, $variation_data, $variation ) {
woocommerce_wp_text_input( array(
'id' => '_variation_expiry_date[' . $loop . ']',
'label' => __( 'Expiry Date', 'woocommerce' ),
'placeholder' => 'YYYY-MM-DD',
'desc_tip' => true,
'description' => __( 'Enter variation expiry date', 'woocommerce' ),
'type' => 'date',
'value' => get_post_meta( $variation->ID, '_expiry_date', true ),
) );
}
// Save variation expiry date
add_action( 'woocommerce_save_product_variation', 'save_variation_expiry_date', 10, 2 );
function save_variation_expiry_date( $variation_id, $i ) {
if ( isset( $_POST['_variation_expiry_date'][$i] ) ) {
update_post_meta(
$variation_id,
'_expiry_date',
sanitize_text_field( $_POST['_variation_expiry_date'][$i] )
);
}
}
This adds an expiry date field to each variation's settings. Each variation can have its own expiry date, and expired variations can be hidden while keeping other variations available.
Modify the query filter from earlier to check variation meta instead of parent product meta for variable products.
The logic gets more complex because you need to check if ANY variations are still valid before hiding the parent product.
Warning: Hiding all variations makes the parent product appear out of stock, even if it technically exists. WooCommerce shows "This product is currently out of stock and unavailable" when no variations are purchasable.
For frontend display, show the earliest expiry date among all available variations. This gives customers the worst-case scenario – they know at least one variation expires soon, even if others have a longer shelf life.
What About Batch/Lot Tracking?
Full batch/lot tracking is beyond basic expiry management. Single expiry date per product or variation handles most store needs.
True batch tracking means managing multiple units of the same SKU with different expiry dates simultaneously – like receiving 100 units on Monday (expires April 2026) and 100 more units on Friday (expires June 2026).
You need to track which units ship first and ensure FEFO (First Expired, First Out) logic.
Neither custom code nor expiry plugins typically handle this. You need:
- Inventory management at the unit level, not product level
- FEFO logic in your order fulfillment process
- Barcode or lot number tracking for traceability
- Dedicated inventory or WMS (Warehouse Management System) integration
When simple expiry tracking is enough:
- You receive inventory in batches but sell out before restocking
- You can track expiry by variation (different variations represent different batches)
- You're willing to use the earliest expiry date for the entire product
- You don't have regulatory requirements for lot traceability
When you need more:
- Multiple batches of the same SKU in stock simultaneously
- Compliance requires batch/lot traceability (pharmaceuticals, food manufacturing)
- You need automated FEFO to ensure the oldest inventory ships first automatically
- You're managing hundreds of SKUs with rapid turnover
For those complex scenarios, look beyond WooCommerce-specific solutions. Dedicated inventory management systems or WMS platforms integrate with WooCommerce but handle the heavy lifting of batch tracking, warehouse locations, and compliance reporting.
Plugin Alternatives (Quick Overview)
Custom code works great when you're comfortable with PHP and want full control.
But sometimes plugins make more sense: you don't have developer access, you need ongoing support, or your requirements are complex enough that building it yourself would take weeks.
Product Expiry Manager for WooCommerce(Free) handles expiry date fields, automatic hiding of expired products, email alerts, frontend warnings, and variation support without writing code. Best for stores with many expiring products that need automation but don't want to maintain custom code.
WooCommerce Stock Manager($79) and WooCommerce Product Batch Numbers($39) include expiry tracking features.
For batch/lot tracking specifically, look beyond WooCommerce plugins. You need dedicated inventory management systems or WMS integrations.
These are significant investments, only necessary for complex operations with regulatory requirements.
Tip: Test any plugin on a staging site first. Some inventory or expiry plugins conflict with existing custom product fields, causing data loss or unexpected behavior. Always back up before installing.
Conclusion
Start by implementing the foundation: add the expiry date field and display it on product pages. Without these basics, nothing else works.
Once you're tracking expiry dates, add the automation that makes sense for your store – automatic hiding of expired products, customer warnings, or admin alerts.
The plugin versus code decision comes down to maintenance: if you're comfortable with PHP and want control, the code approach works well. If you need ongoing support or don't have developer access, a plugin saves time.
Most stores selling time-sensitive products need at least 3-4 of these features working together. Build the system in stages, test thoroughly, and expand as your inventory grows.