WooCommerce Sequential Order Numbers: Why IDs Skip and How to Fix It
WooCommerce order numbers are database IDs, shared with posts, products, refunds and checkout drafts, so they jump. You cannot make the ID sequential, but you can change the number customers see — and there are three places where that change has to be honoured.
Some links on this page are affiliate links: if you buy through one, we may earn a commission at no extra cost to you. It does not change what we recommend; see the editorial policy.
WooCommerce order numbers skip because they are database IDs, and you cannot make the ID sequential — but you can change the number customers see. The order number is whatever get_order_number() returns, and by default that is the ID. Below is why the ID jumps, how to show your own sequence instead, and the places that still look orders up by ID and will not recognise the new number.
Why the IDs jump
WC_Order::get_order_number() returns the order ID passed through the woocommerce_order_number filter. Nothing more. The ID itself is allocated from a counter the order shares with other content:
- On the classic posts storage, an order is a post of type
shop_order, so its ID comes from the same sequence as every post, page, revision, attachment, menu item, product and variation. - With High-Performance Order Storage, orders live in their own table, but WooCommerce still inserts a post for each one — a placeholder, or a full copy when compatibility sync is on — so the ID is still reserved in the posts table.
- Refunds are orders. Each refund is a
shop_order_refundrecord with its own ID. - The block checkout creates draft orders with the
checkout-draftstatus while a shopper is on the checkout page. Drafts that are never completed are deleted after about a day, and their IDs are gone.
So a quiet store that publishes a blog post, edits a product and loses an abandoned checkout between two sales can go from order 1043 to 1051. The gap means nothing is wrong.
What you can change
You cannot renumber the ID. It is the primary key orders are stored and linked by, and changing it would break order notes, refunds, subscriptions and anything else that references the order.
What you can change is the displayed number, because everything that shows a number to a person — the orders list, emails, My Account, the thank-you page — calls get_order_number(). Store your own number on the order when it is created, and return it from the filter:
add_action( 'woocommerce_new_order', 'ti_assign_order_number', 10, 2 );
function ti_assign_order_number( $order_id, $order ) {
global $wpdb;
if ( $order->get_meta( '_ti_order_number' ) ) {
return;
}
// Atomic increment, so two simultaneous orders never get the same number.
$wpdb->query( $wpdb->prepare(
"UPDATE {$wpdb->options} SET option_value = LAST_INSERT_ID( option_value + 1 ) WHERE option_name = %s",
'ti_order_counter'
) );
$next = (int) $wpdb->get_var( 'SELECT LAST_INSERT_ID()' );
$order->update_meta_data( '_ti_order_number', 'TI-' . $next );
$order->save_meta_data();
}
add_filter( 'woocommerce_order_number', 'ti_display_order_number', 10, 2 );
function ti_display_order_number( $number, $order ) {
$custom = $order->get_meta( '_ti_order_number' );
return $custom ? $custom : $number;
}
Create the counter once with add_option( 'ti_order_counter', 1000, '', false ); the first order then becomes TI-1001. If the option is missing, the UPDATE matches no row and LAST_INSERT_ID() returns 0 or an unrelated value, so create it before the snippet goes live. The atomic UPDATE matters: reading the option, adding one and saving it lets two orders placed in the same second share a number. Use $order->get_meta() rather than get_post_meta(), so it works on both storage types.
woocommerce_new_order does not fire for draft statuses, so block checkout drafts never consume a number; the hook fires when the draft becomes a real order. When it fires and what the order contains at that moment is covered in woocommerce_new_order.
Three places that still use the ID
Admin search. Searching the orders list for TI-1001 finds nothing by default, because both storage types search only a fixed set of meta keys. Add yours with the woocommerce_shop_order_search_fields filter on the posts storage and woocommerce_order_table_search_query_meta_keys with HPOS. Storing the full formatted number, prefix included, as the snippet does, is what makes that search match.
Order tracking. The [woocommerce_order_tracking] shortcode takes the number the customer types and loads the order by ID, through the woocommerce_shortcode_order_tracking_order_id filter. Without a filter that maps your number back to the ID, the tracking form fails for every new order.
Payment gateways and integrations. Some send the order number to the payment processor as the reference, some send the ID. Check which one appears in your gateway dashboard and in your accounting export, so the two can be reconciled.
Order numbers are also not invoice numbers. Where invoices must be numbered without gaps, give them their own sequence, as explained in WooCommerce invoices.
When a plugin is the better answer
The snippet covers a plain sequence. Prefixes that include the date, a sequence that resets each year, separate numbering for free orders, and search and tracking support for all of them add up to code you then maintain. Sequential Order Numbers for WooCommerce is one plugin that covers the numbering side: custom prefix and suffix, a starting number and increment, the order date in the number, a separate sequence for free orders, daily, monthly or yearly resets, and existing orders keep their numbers. Whatever you install, check that it declares compatibility with High-Performance Order Storage — WooCommerce lists incompatible plugins on its features settings — and run the checks below.
Before switching, decide what happens to existing orders. Leaving them on their IDs and numbering only new orders is the safe choice; renumbering history breaks references in old emails and in customer conversations. If you are moving to a new store at the same time, the order data side is in how to migrate a WordPress site.
Common mistakes
- Trying to reset the ID with
ALTER TABLE … AUTO_INCREMENT. It changes only future IDs, MySQL will not set it below the highest existing ID, and the counter belongs to the whole posts table. - A non-atomic counter, which gives two concurrent orders the same number.
- Forgetting the tracking form and admin search, so the new number is shown everywhere and found nowhere.
- Using
get_id()in custom code where the customer-facing number was meant. Useget_order_number()for anything a person reads. Hooks such as woocommerce_thankyou pass the ID, so load the order and ask it for the number.
Verify
- Place two test orders and confirm consecutive numbers in the orders list, the emails and My Account.
- Search the admin for the new number.
- Look the order up through the tracking form.
- Refund one order and confirm the next order's number did not skip.
- Check the reference your payment gateway recorded. </content>
Frequently asked
- Not the order IDs. They come from a database counter shared with posts, pages and products, and MySQL will not set that counter below the highest ID already in the table. You can start your own displayed sequence at any number by storing it on each order and returning it through the woocommerce_order_number filter.
- Because WooCommerce still creates a placeholder post for each order to reserve its ID, so the ID comes from the posts table either way. Refunds and checkout drafts are orders too and take numbers from the same counter.
- Only where code looks the order up by the number rather than the ID. The admin search and the order tracking shortcode work with IDs by default, so a custom numbering scheme has to map the number back to the ID in those places.