Rows of server racks with blinking lights in a data centre

Exporting 79,354 products in 17 seconds, and editing 500 in 8

We wanted bulk edits to be ten times faster. WooCommerce's own save sets the floor, so we measured it, sped up everything around it, and made the writes safe. Real numbers from a store with 79,354 products and variations.

We exported every product and variation of a WooCommerce store with 30,002 products and 49,352 variations. Vibery Sites finished the whole file in 16.8 seconds. The first version of this export took 169 seconds. The path before that, which reads every page live from WooCommerce, we projected at 608 seconds.

The number we were chasing was a different one. We wanted bulk edits to be ten times faster. They are not, and this post is about why, and what we did instead.

The floor we could not go below

Before changing anything, we measured the floor. In-process, no network, 300 products on the same 400,000-order store, we changed a price and saved it through WooCommerce's own product save, then reverted:

Method

Time per product

WooCommerce's own save

10 to 13 ms

Same, with term and cache work deferred

about 10 ms

Direct SQL on the price fields

3 ms

Direct SQL is four times faster, and we do not use it. It skips WooCommerce's hooks, so other plugins, caches and search indexes would never see the change. Your WordPress site stays the source of truth, so every write goes through WooCommerce's own save.

Our bulk edit cost about 31 ms a row. WooCommerce's own save is about a third of that. So the honest ceiling was about 3×, not 10×.

What we changed

We did three things.

1. We stopped reading through WooCommerce. Exports, the sales preview and the snapshots we take for undo all used to page through WooCommerce live. We already hold those rows. They now come from our copy, and a snapshot is only trusted after one cheap check per 100 rows proves it is current. Rows that changed on the site are read live.

On the 79,354-row store:

Before

Now

Full CSV export, 79,354 rows

608 s (projected)

16.8 s

Sales preview, one category (first 1,000 products)

5.25 s live

0.5 s

A preview of 200 chosen products takes 0.33 seconds. The export has a footnote. The first export after the change took 794 seconds, once, because older rows in our copy lacked some fields and were topped up from the store while it ran. After that, every export is the 16.8 seconds.

2. Big changes run in the background. Editing 500 products used to freeze the screen for 17 seconds. Now the request is answered in 1.6 seconds with a job and a restore point already in place. The bar fills while you keep working. You can stop it after the current batch, and anything it wrote can be undone.

We tried the awkward cases on the real store. We started a 500-row edit and stopped it after the first batch: it finished the 200 rows in flight, left 300 untouched, and undo put those 200 back exactly. A second change started while one was running was refused with a clear message, not queued behind it. In a 32-row job with a made-up product id and a duplicated one, both bad rows were rejected before anything was written, and the other 30 went through and were undone.

3. A batch that writes safely. Our WordPress plugin gets a bulk endpoint that takes up to 100 product changes at once and saves each through WooCommerce, inside one database transaction. All rows are checked first. If any row fails, the whole batch rolls back and nothing is left half done. A repeated request with the same key is applied once, which matters for a "10% off" that would otherwise apply twice after a timeout.

We forced failures to see it hold: an error on row 60 of 100, an exception on row 30, and an invalid id in the batch. Each left the store exactly as it was, and the change log held nothing from the rolled-back rows.

Speed, same 500 products, same store:

WooCommerce batch endpoint

Our transactional batch

Edit 500 products

17.5 s (30.6 ms/row)

8.3 s (12.2 ms/row)

Undo it

15.2 s

8.3 s

That is about 2.1×. A plain WooCommerce loop in PHP runs at 10 to 13 ms a row, so our batch is only about 20% faster than the floor. We are not hiding that. The gain came from removing round trips and from doing all 100 rows in one transaction.

What the log shows

This is straight from the job record of the 500-row edit:

write_path: plugin
plugin_chunks: 5   plugin_requests: 5   fallbacks: 0   retries: 0
plugin_ms_per_row: 12.2

And the same edit on the older path:

write_path: wc_batch
chunks: 10   ms_per_row: 30.6

What we did not do, and what we do not know

  • The transactional batch is not released yet. It ships with the next plugin update. The faster export and preview and the background jobs are live now.
  • One store, one server. All timings are from a single hosted store on a shared server. A different host will differ.
  • Stock quantity changes still go through the slower path, because they have to write WooCommerce's stock history.
  • Third-party plugins that call outside services during a product save cannot be undone by a rollback. We run those after the commit.

We set out to be ten times faster and found that WooCommerce itself would not let us. The faster part was everything around the save. If your store has a bulk job you dread starting, what would make you press the button?

No comments yet