How We Reduced Priority1 API Requests by 92% in a WooCommerce LTL Shipping System

How We Reduced Priority1 API Requests by 92% in a WooCommerce LTL Shipping System

Sometimes a performance problem is not caused by a slow server, a bad database query, or an expensive piece of code.

Sometimes the problem is much simpler:

We were asking the same external service too many times.

This is the story of a shipping optimization I worked on for JMA Attachments, a US-based WooCommerce project that sells heavy equipment attachments.

You can see more about the project on the JMA Attachments project page.

The task initially looked like a normal refactoring job around shipping calculations. It turned into a much more interesting investigation into API usage, batching, caching, duplicated calculations, and the difference between what the code could do and what the production system was actually doing.

And, as it often happens in software development, there was a little surprise waiting inside the codebase.

The Shipping System

Shows Priority1 Company Logo

JMA Attachments uses Priority1, a US freight and LTL shipping service, to calculate shipping rates for products that are too large or heavy for normal parcel shipping.


The WooCommerce flow needed to calculate shipping information in several places:

  • Cart
  • Single Product / Add to Cart flow
  • Checkout
  • Shipping method recalculation
  • ZIP code changes
  • Quantity changes

The site also had a feature that allowed users to get an approximate shipping price early in the shopping journey.

The user could provide a ZIP code, and the system could use it to estimate the shipping cost. If no ZIP code was available, the site could also try to identify the location from the user’s IP address.

On paper, this sounds reasonable.

The problem appeared when we looked at how many times the external API was actually being called.

1. Why Was Optimization Necessary?

At checkout, every LTL product in the cart could generate its own API request to Priority1.

Not one request for the cart.

One request per product.

For example, a cart with three LTL products could generate approximately six API requests:

  • one request for the shipping rate
  • one request for the freight class

And this was not necessarily the worst case.

Depending on what the customer was doing, a single interaction could generate 9–15 API requests.

Changing the ZIP code?

More requests.

Changing the quantity?

More requests.

Changing the shipping method?

More requests.

Refreshing the page?

Potentially more requests.

At some point, the API was basically becoming another participant in the checkout process.

And it was participating very enthusiastically.

The structural problem

The interesting part was that the project already had a batching system.

A previous implementation had been designed to support a more complex use case involving multiple shipping addresses.

Later, the business logic was simplified to use a single shipping address.

The batching architecture, however, was not completely removed.

Instead, parts of it remained in the codebase — but the production execution path was no longer using it.

So we had something slightly ironic:

The project already had the solution. It just wasn’t connected to the road that production traffic was using.

The active path was still effectively:

Product → Priority1 API
Product → Priority1 API
Product → Priority1 API

Instead of:

Cart → Priority1 API

This is one of those problems that is easy to miss during normal development because the code can look perfectly reasonable when you inspect one product at a time.

The problem only becomes obvious when you look at the entire request lifecycle.

The Real Impact

This was not just about making the code cleaner.

The excessive API calls had real production consequences.

The initial estimate showed approximately $1,800/month in API costs.

There were also:

  • longer Cart and Checkout loading times
  • a relatively high API error rate of around 18%
  • production timeout problems around the existing 15-second timeout
  • unnecessary repeated calculations
  • duplicated freight-class requests

The second API call was particularly interesting.

The system was also asking Priority1 to determine the freight class of the product.

So the shipping process was effectively doing something like:

Product
   ↓
Get freight class
   ↓
Get shipping rate

for every relevant product.

That meant the number of external requests could easily double.

2. What We Changed

The optimization was not based on one magic trick.

It was a combination of several smaller changes that all had the same goal:

Do not call the external API unless we actually need to.

2.1 Real API Batching

The first and most important change was to activate and repair the existing batching architecture.

Instead of:

Product A → API
Product B → API
Product C → API

the system could send the cart information together:

Cart
 ├── Product A
 ├── Product B
 └── Product C
        ↓
   Priority1 API

This reduced the number of API calls from one per product to effectively one call for the complete cart calculation.

However, simply reconnecting the batching system was not enough.

There were also bugs inside the existing implementation.

For example:

  • the liftgate logic was inverted
  • the freight class was hardcoded in part of the flow

So the batching code was not just disconnected.

It also needed some repairs before it could safely become the production path.

2.2 Multiple Cache Guards

The next problem was repetition.

Even after batching, the same cart could still trigger the same calculation multiple times during a request lifecycle.

WooCommerce can trigger shipping calculations more often than you might expect.

A customer changes a ZIP code.

WooCommerce recalculates.

A quantity changes.

WooCommerce recalculates again.

A shipping method changes.

Another calculation.

The page refreshes.

You guessed it.

Another calculation.

So I added multiple layers of protection.

The main idea was:

If nothing relevant changed, there is no reason to ask Priority1 again.

The implementation used:

  • session-level caching
  • static caching within the current request
  • guards based on cart contents
  • quantity changes
  • shipping address / ZIP changes

This meant that the system could recognize when it already had the answer for the current state of the cart.

Instead of:

Calculate
Calculate again
Calculate again
Calculate again

we wanted:

Calculate
Use existing result
Use existing result
Use existing result

This sounds obvious.

In a production WooCommerce system, it is surprisingly easy for the first version to become the second one.

2.3 Removing the Freight-Class API Call

The second major optimization was more interesting.

We asked a simple question:

Do we really need to call Priority1 to calculate the freight class?

The answer turned out to be no.

Instead of asking the external service for the freight class for every product, we built an internal calculator based on the NMFC density formula.

The important part was validation.

We did not simply assume that our formula was correct.

The locally calculated results were compared product by product against the real Priority1 results.

For the tested sample, the results matched 100%.

After the local calculation was introduced, the freight-class API requests disappeared from the traffic completely.

That means this:

Product
   ↓
Calculate freight class → Priority1
   ↓
Calculate shipping rate → Priority1

became:

Product
   ↓
Calculate freight class locally
   ↓
Priority1 API → shipping rate

One external dependency was removed entirely.

That is usually better than trying to make an unnecessary external dependency faster.

2.4 Increasing the Timeout

During production testing, we also noticed that some Priority1 responses could be slow.

The existing timeout was 15 seconds.

That was not enough for some real-world cases.

The timeout was increased from:

15 seconds

to:

30 seconds

This was not intended to solve the underlying performance problem.

It was a defensive adjustment.

There is an important difference between:

“Let’s wait longer.”

and:

“Let’s make fewer requests, and when we actually need one, give the external service enough time to respond.”

The second approach was the real optimization.

2.5 Avoiding Unnecessary Requests

Additional guards were added around the shipping address.

If the ZIP code was missing, the system would not blindly call the external service.

If the ZIP code had not changed from the previous request, the existing result could be reused.

Products that did not need LTL shipping were also removed from the Priority1 flow.

Some lightweight products had previously been classified in a way that caused them to enter the LTL process even though they did not need to.

So another optimization was simply:

Don’t send products to the LTL system when they don’t belong there.

Sometimes the fastest API request is the one your code never makes.

2.6 We Also Tested a More Complex Cache

There was another idea that we explored: persistent caching at the database level.

The idea was to share cached shipping results between users and sessions.

Technically, this could have reduced even more API requests.

But after testing the approach, we decided not to use it.

The session-level solution was already sufficient for the actual traffic and requirements.

This was an important engineering decision.

More architecture does not automatically mean better architecture.

We could have introduced another persistent caching layer, cache invalidation rules, shared state, expiration strategies, and more complexity.

Instead, we asked:

Do we actually need it?

The answer was no.

So we kept the simpler solution.

That’s an optimization too.

What We Did Not Do

One important part of the solution was what we didn’t implement.

We did not solve the API problem with aggressive retry logic or reactive rate limiting.

The strategy was not:

Make too many requests
        ↓
Hit the limit
        ↓
Retry
        ↓
Wait
        ↓
Retry again

The strategy was:

Need calculation?
      ↓
Did something relevant change?
      ↓
No → Use cached result
Yes
 ↓
Batch the request
 ↓
Call Priority1 once

The goal was to reduce the number of requests at the source.

Not to build a sophisticated system for surviving unnecessary requests.

3. The Results

The most important part of an optimization story is not the code.

It is the measurement.

After the changes, the number of Priority1 API requests dropped dramatically.

Between April and May 2026, requests decreased from approximately:

407 requests/day → 32 requests/day

That is approximately a 92% reduction.

And there is an important detail here.

These numbers were not based only on internal application logs.

The reduction was also confirmed independently using the actual Priority1 billing data.

That distinction matters.

API Error Rate

The API error rate also improved significantly:

Before: ~18%
After:  ~3%

That’s roughly a 5× reduction in the error rate.

Again, the interesting part is not simply that the number became smaller.

It makes sense when you look at the architecture.

Fewer unnecessary requests mean:

  • less API traffic
  • fewer opportunities for transient failures
  • fewer concurrent requests
  • less pressure during checkout recalculations

Freight-Class Requests

The freight-class API calls went from:

> 0

to:

0

They were completely removed after moving the calculation into the application.

Checkout Rate Requests

There were also some smaller but useful improvements.

For example, checkout rate requests that previously happened twice per page load were reduced to one actual request.

Another calculation that previously triggered shipping recalculation approximately six times per request was reduced to a single effective API call.

These individual improvements may sound small.

Across a busy WooCommerce store, they add up.

A Small Lesson About Measuring Performance

There is one thing I would specifically mention when documenting this kind of optimization.

Be careful with simple “before vs after” numbers.

Initially, comparing the total number of API calls before and after the changes could give a misleading picture because traffic had increased significantly during the same period.

So if we simply said:

“There were fewer API requests after the optimization.”

that would not tell the complete story.

The stronger evidence came from comparing the application’s behavior with the actual Priority1 billing data.

That gave us an independent measurement of how much traffic was really reaching the external service.

This is an important lesson for performance work:

Do not optimize the graph you happen to have. Understand what the graph actually measures.

Traffic changes.

Users change.

Products change.

Marketing changes.

Business volume changes.

A good optimization should survive those variables as much as possible.

What This Project Taught Me

The most interesting part of this optimization was not the batching itself.

It was discovering how easily a production system can drift away from the architecture that was originally designed for it.

The code already contained a batching solution.

It had simply become disconnected from the real execution path.

This is common in long-lived projects.

A feature evolves.

A business requirement changes.

An architecture designed for five scenarios is later used for one.

Some code becomes unused.

Another path becomes the “temporary” solution.

The temporary solution survives for a year.

Then somebody investigates the API bill.

And suddenly that temporary solution becomes very interesting.

A few lessons I would keep from this project:

1. Measure the real execution path.

Don’t assume the architecture you see in the code is the architecture production is using.

2. Batch before adding complexity.

If an external service supports batching, use it before trying to solve the problem with retries, queues, or rate limiting.

3. Cache based on meaningful state.

If the cart, quantity, or shipping address has not changed, recalculating the same result is usually unnecessary.

4. Remove external dependencies when possible.

If a calculation can be performed locally and validated against the external service, that can be much better than making another API request.

5. Complexity has a cost.

We tested persistent database-level caching and decided that session-level caching was enough.

The more sophisticated solution was not automatically the better solution.

6. Validate with independent data.

Internal logs are useful.

External billing data is even more useful when you are trying to understand actual API consumption.

Final Thoughts

This optimization started as a request to refactor shipping calculations.

It ended up being a good example of something I see quite often in mature WordPress and WooCommerce projects:

performance problems are frequently architectural problems rather than “slow code” problems.

The final solution was not a huge rewrite.

We did not replace WooCommerce.

We did not introduce a new infrastructure platform.

We did not build a distributed caching system.

We did not add five more plugins to solve the problem.

Instead, we:

  • repaired an existing batching architecture
  • reduced duplicated calculations
  • introduced cache guards
  • removed an unnecessary external API call
  • corrected shipping logic
  • filtered products that did not need LTL processing
  • adjusted the timeout for real production conditions
  • validated the results against external billing data

The final result was approximately 92% fewer Priority1 API requests, a drop in API errors from roughly 18% to 3%, and the complete removal of freight-class API requests.

And perhaps the biggest lesson was the simplest one:

Before building a smarter system, make sure the existing system is actually using the smart part.

Sometimes the optimization is already sitting there in the codebase.

It is just waiting for somebody to connect the wires.

Have a similar challenge?

I take on a limited number of consulting engagements for problems like this one — architecture reviews, performance audits, or hands-on implementation.

Let's Talk