OPS · CHECKOUT · 3PL · 8 AUGUST 2026 · 8 MIN READ
Shipping rules that match how you actually ship
Most complicated shipping setups are a diagram of the warehouse pretending to be a checkout. Start from what the customer is allowed to choose, not from how the parcel moves.
Write down the shipping options a customer should see for each type of basket first, then build backwards. Shopify gives you four layers — delivery profiles for products that ship differently, zones and rates inside each profile, a carrier service for live quotes, and a delivery customization function to hide or rename options at checkout. Most "complex" requirements are one profile and two price-based rates. The genuinely hard cases are baskets that span profiles, because Shopify adds those rates together.
IN SHORT
- Shopify combines rates when a basket spans shipping profiles: its documentation gives an example of two profiles each offering a $5 rate producing a $10 charge on one order.
- A delivery profile is the right tool only when a product genuinely ships differently — from another location, by another carrier, or under another cost model.
- Shopify rates can be conditional on weight or on order value, and nothing else; every other condition needs a carrier service or a function.
- A carrier service callback has to answer inside a timeout that tightens as your request rate rises, and Shopify caches identical requests for 15 minutes.
- Delivery customization functions hide, reorder and rename options — they do not invent rates, so they cannot rescue a rate table that is wrong.
Start from the checkout, not the warehouse
Ask an operations team how shipping works and you get the true answer: three locations, two carriers, a pallet rule for anything over 30kg, a next-day cut-off that moves on Fridays, and a supplier who drop-ships four SKUs. All of that is real. None of it is a checkout requirement until you say what the customer sees.
So do that first, on one page. For each kind of basket — normal parcel, heavy item, drop-shipped item, mixed basket — list the options a customer should be offered and the price of each. It takes twenty minutes and it settles arguments that otherwise run for weeks, because it forces a decision on the mixed basket, which is the case everybody skips and the case that breaks.
What you usually find is that the checkout needs fewer rules than the warehouse does. The warehouse cares which of three locations picks a line. The customer cares whether it arrives tomorrow and what it costs. Those are different problems, and routing is the second one's job, not the rate table's.
The four layers, in the order you should reach for them
Shopify's shipping configuration is four distinct mechanisms, and nearly every over-engineered setup we see is someone reaching for layer three when layer two would have done.
- Zones and rates in the general profile. Flat rates, free shipping, and conditions on weight or order value. Shopify documents exactly two condition types: rates "based on the combined weight of items shipping from the same location with the same shipping option", and rates "based on the total value of the order". This layer handles most stores completely.
- Delivery profiles. A separate profile for products that ship differently — heavy goods, drop-shipped lines, anything with its own origin. Profiles hold location groups, zones and method definitions, and products are assigned to them explicitly, with the general profile catching everything unassigned.
- A carrier service. A callback URL that Shopify posts the basket and destination to, and that returns a list of rates. This is how you get live quotes from a carrier, or apply logic Shopify's rate table cannot express.
- A delivery customization function. Shopify Functions that, in the documentation's words, "hide, reorder, and rename the delivery options that are available to customers during checkout". It edits the list; it does not create rates.
The mixed basket, and why profiles are expensive
This is the thing to understand before you create a second profile, because it surprises people at the worst moment.
Shopify's documentation states plainly: "When an order contains products from multiple shipping profiles, Shopify combines the applicable price-based rates." The example given is two profiles each offering a $5 discounted rate over $50, and an order over $50 that draws on both profiles being charged $10 at checkout. Weight-based rates behave the same way, combining across the products involved.
That is correct behaviour — two profiles means two shipments, and two shipments cost twice — but it is rarely what the merchandising team pictured. They pictured free shipping over £50 meaning free shipping. On a basket with one furniture item and one cushion, the customer sees a sum of two rates, and the first thing anybody does is call it a bug.
The practical rule: create a profile when the product genuinely ships as its own consignment, and accept that a mixed basket will be charged accordingly. If the product ships in the same parcel as everything else and you only wanted a surcharge, do not use a profile — use a weight-based rate, or bake the cost into the price of the product, which is invisible and never disputed.
When you actually need a carrier service
A carrier service is the right answer when the price genuinely cannot be known from weight and basket value: dimensional weight, live carrier quotes, per-postcode surcharges, or a freight rule based on pallet count. It is the wrong answer when someone simply wants a third condition type and has decided code is easier than a conversation.
The reason to be reluctant is that it sits on the critical path of every checkout. Shopify's documented timeouts tighten as your traffic rises — 10 seconds below 1,500 requests per minute, 5 seconds between 1,500 and 3,000, and 3 seconds above 3,000. There is no retry: the documentation is explicit that the request has to succeed first time, and that a 40x or 50x response triggers your backup rates. Peak trading is exactly when your rate service is slowest and exactly when the timeout is shortest.
Two things follow. Set backup rates that you would be willing to honour on your worst day, because that is when they will be shown. And lean on the caching — Shopify caches identical requests for 15 minutes, keyed on variant IDs, quantities, weights, origin and destination addresses and the carrier service ID, with a 30-second cache on errors — which means your service should be deterministic for the same input rather than quoting a different number each call.
Worth checking before you scope any of it: carrier-calculated shipping requires an Advanced plan or above, a Shopify plan billed yearly, or the carrier service add-on. Development stores have it, which is how teams get a long way into a build before discovering the production store does not.
What a function is good for
Delivery customization functions are the cheapest layer and the most under-used. They do three things: hide an option, reorder options, and rename them. That covers a surprising share of real requirements.
Hiding is the common one — suppress express delivery for a postcode that cannot receive it, or for a basket containing an item that ships on a pallet. This is far better than trying to express the exclusion in the rate table, because a rate table can only add options, not remove them conditionally.
Renaming is underrated. "Standard" and "Express" mean nothing; "Arrives Thu 14 Aug" and "Arrives tomorrow if you order in the next 3 hours" are the same two rates doing considerably more work. If your carrier data can support a date, put the date in the label.
Worth heeding one warning in Shopify's own guidance: checkouts and orders can include multiple delivery methods, so do not write a function that assumes one method per order. That assumption holds in testing and fails on the first basket that spans two profiles.
The parts that are not a rate problem
Three requests arrive dressed as shipping rules and are not.
"Ship from the nearest warehouse." That is order routing, decided after the order is placed, and it should not change what the customer was quoted. Keep the customer-facing price stable and let routing optimise cost behind it.
"Different cut-off times per carrier." That is a delivery promise, not a rate. It belongs on the product page and in the option label, where it changes behaviour, rather than in a rate condition where nobody sees it.
"Free shipping for our trade customers." That is a customer-level entitlement. Shopify's B2B tooling and discounts handle it more cleanly than a duplicate rate table, and a duplicate rate table will drift within a quarter.
How we would sequence the work
Build the simplest thing that serves the four basket types you wrote down, and ship it. One profile, two or three rates, conditions on value or weight. Then watch: support tickets about shipping cost, checkout abandonment at the shipping step, and the gap between what you charge and what you pay the carrier.
Add a profile only when a product line really is a separate consignment. Add a carrier service only when the number genuinely cannot be derived. Add a function when there is an option somebody should not be seeing. Each of those is a component with an owner, a failure mode and a monitoring requirement — which is the part nobody budgets for and the reason we would talk you out of the elaborate version until the simple one has demonstrably failed.
Questions this raises
How do you configure complex shipping rules on Shopify?
In four layers, cheapest first. Zones and rates in the general profile, with conditions on weight or order value, cover most stores. Delivery profiles separate products that ship as their own consignment. A carrier service supplies rates that cannot be derived from weight or basket value. A delivery customization function hides, reorders or renames the options a customer sees.
Why is my customer being charged shipping twice?
Almost certainly because the basket spans two delivery profiles. Shopify's documentation states that when an order contains products from multiple shipping profiles, it combines the applicable rates — its own example shows two $5 rates producing a $10 charge. It is working as designed; the design decision was creating the second profile.
Can Shopify rates depend on anything other than weight and price?
Not natively. Shopify documents weight-based and order-value-based conditions only. Anything else — dimensions, postcode surcharges, pallet counts, carrier quotes — needs a carrier service returning rates from your own logic, or a function to hide options the rate table has already produced.
What happens if our carrier service is slow or down?
Shopify shows your backup rates. The documented timeout tightens with request volume — 10 seconds under 1,500 requests per minute, 5 seconds up to 3,000, 3 seconds above that — and there is no retry. Set backup rates you would be content to honour during peak, because peak is when they will appear.
Do we need Shopify Plus for delivery customization functions?
Shopify's guide on delivery options does not present functions as Plus-only; the Plus restriction it states applies to checkout UI extensions rendering on the information, shipping and payment steps. Carrier-calculated shipping is the one with a clear commercial gate: an Advanced plan or higher, a yearly-billed Shopify plan, or the add-on.
Should shipping rules follow how we route orders internally?
No. Routing decides which location picks the order after it is placed and should be free to optimise cost. The rate a customer saw is a promise made at checkout. Coupling the two means every warehouse change becomes a pricing change, which is a bad trade in both directions.
NEXT STEP
Free store audit
A senior Shopify engineer reviews your storefront, theme performance and checkout, then sends a prioritised list of fixes.
