What WooCommerce Subscriptions Actually Has to Do
Recurring billing means retries, dunning, proration, tax on renewals and expired cards. What each of the four WooCommerce subscription approaches takes on.
Ana Ferreiro · @aferreiro
WooCommerce engineering
- Topic
- WOO
- Section
- WooCommerce & Storefront
- Published
- 17 OCT 2026
- Reading time
- 9 MIN
- Words
- 2163W
Month four is when a store finds out what it bought. Months one to three renew cleanly on the card
that signed up, and the setup looks finished. In month four a card that expired in March declines a
renewal, the renewal order sits in failed, and the retry meant to recover it is queued
behind WP-Cron, which runs when a request arrives, so on a quiet week it can land a week late.
The customer updates the card, the renewal order is still failed, and the button that would charge it again is not obvious, so support creates a new order for the same month and charges it. It clears. Forty minutes later the original retry fires against a working card and charges again.
Two parts of the system each did the locally sensible thing and neither knew the other existed. Nothing threw an error.
Disclosure: some links on this page are affiliate links, and WPSifero may earn a commission if you buy through them. That does not change the price you pay, and it does not change what this piece recommends: the approach that is wrong for a small store is named as wrong below.
An order is a moment, a subscription is a relationship
WooCommerce core assumes one payment per order: stock decrements once, a coupon applies once, tax is calculated once, and the reports assume the payment happened. A subscription is the opposite shape. It produces orders over time, and the part that matters is the twenty-fourth, not the first.
So the extension is not buying a product that repeats. It is buying a state machine: which period a charge covers, when the next attempt happens, who tells the customer, and what happens when two events describe one payment out of order.
The four hard problems, in the order they cause trouble
Retries and dunning. A decline is not one event. An insufficient balance on the twenty-eighth is soft and may clear in three days; a reported stolen card is hard and never will. Retrying both on one schedule sends attempts at a card the bank has already blocked, and teaches a customer that the store is still chasing a payment they have already replaced. A workable schedule has a budget, a reason per attempt, and a stop condition that ends the subscription. Somebody has to send the emails, and exactly one somebody: a store running the extension's dunning and the gateway's dunning sends two apologies with different next steps.
Proration. A customer upgrades on day nine. Charge the difference for the remaining twenty-one days, charge a month and credit the unused part, or let the old period run out. All three are defensible; the invoice has to say which happened. Downgrades produce a credit rather than a charge, which is where stores discover that their refund process only handles refunds. An annual to monthly switch can prorate to more than a monthly payment and fail on a card's daily limit.
Tax on renewals. A renewal order is a new order, priced by the store's settings on the day it is generated, not those that applied at signup. That is correct and also a surprise: a store that changed its rates charges a renewal that does not match the signup invoice, and the customer compares the two. Strong customer authentication has to be its own state rather than a generic failure with a retry attached. In many jurisdictions each period's invoice has to stay retrievable for years.
Card expiry. The card networks pass replacement numbers to the processor, so whether a renewal survives an expiry depends on whether the extension asks for the updated card before it charges. The difference stays invisible until the January after signup, when a cohort of cards issued two years earlier fails at once. The customer reads that as the store cancelling a subscription without telling them.
The failure with no error message
The first failure is a month-four renewal nobody retries, and it is quiet. A failed renewal looks like a customer who stopped paying, and the dashboard counts new orders rather than renewals that never happened. The revenue vanishes at the end of the period, not on the day of the failure.
The second is the double charge, and it happens because of the first. The manual fix and the scheduled retry are independent attempts at one payment with no shared key. The invariant was never written down: one charge per subscription per period, keyed so a second attempt is recognisable as a second attempt.
The procedure that avoids it is rarely documented:
- Find the existing renewal order and charge that order, not a new one.
- Cancel the scheduled retry in the same action, not a follow-up ticket.
- Check the gateway for a charge covering the same period first.
- Record who charged what, and why, in the order notes.
None of that is plugin behaviour; it is store procedure. What an extension can do is make it possible: keep the failed renewal order instead of deleting it, show the gateway transaction reference on it, and let the retry be cancelled from the screen that charges it by hand. A store that chooses an extension before checking those three has bought a feature list. A daily count of renewals past due then finds the double charge a day before the customer does.
Four approaches, and what each one takes on
They differ less in features than in who keeps the ledger.
| Approach | Takes responsibility for | Still the store's problem |
|---|---|---|
| Official extension | Renewal orders, retries, proration, account portal | Scheduler health, monitoring, manual charges |
| Hosted billing layer | Invoices, retry ladder, card updates, tax | Entitlement webhook, refunds, revenue reporting |
| Membership plugin | Access control, gating, its own gateway | Stock, shipping, coupons, order reporting |
| Payment API by hand | Only what is written in-house | Everything else, including whoever is on call |
The official extension keeps renewals inside WordPress
WooCommerce Subscriptions, in the WooCommerce Marketplace, puts the renewal inside the store. It owns the subscription record, a renewal order per period, a retry schedule with configurable attempts, proration for switches, a renewal date synchronised to the calendar so a customer who signs up on the 31st does not drift through short months, and an account page for card replacement and cancellation.
The property worth paying for is that renewal orders are ordinary orders. Stock decrements, coupons apply, shipping rules run, and the reports keep adding up, because a renewal is not another system's event mirrored into WordPress but a WooCommerce order with a subscription behind it. For a store shipping goods on a schedule, that is most of the decision. Pricing is an annual licence per site rather than a share of revenue; check the current price, they change it.
What it does not own is everything around the state machine. It does not make a retry fire on a host that strangles WP-Cron. It does not stop support creating a second order. It does not tell anybody that eleven renewals failed overnight. There is no free edition either: the free option keeps the renewal out of WooCommerce and charges it in the provider's own invoicing. For one plan and a few dozen subscribers that is the correct first version.
A hosted billing layer removes the retry ladder
The hosted options invoice and retry on somebody else's infrastructure, whether they come from a processor or a standalone billing vendor. They own the invoice as the source of truth, a retry ladder tuned across thousands of merchants, card account updates, proration, a tax engine that keeps up with rate changes, a customer portal, and reports an accountant accepts.
They leave the seam. WordPress stops being the ledger and becomes a mirror: access is granted by a webhook, so entitlement becomes a small distributed system. The webhook has to be idempotent because the event arrives twice, has to revoke access when a refund is issued in the hosted dashboard, and needs a reconciliation job for the case where nothing arrives. The other cost is reporting: renewals that never become orders leave the revenue report half true, coupons do not apply, and which products earn gets two answers.
That is a fair trade for software or a course, where nothing ships, and a bad one where margin depends on knowing which box went out. The free alternative is the processor's own recurring invoices, included with the account: enough for one plan and a customer list short enough to read.
A membership plugin treats access as the product
The membership plugins start from entitlement: which account has which role, which content is visible, when access begins and ends, all of it funded by a recurring charge. That is right when access is the product, because the boundary is paid against unpaid and the renewal is a supporting actor. It is wrong when the thing sold is physical, since a membership plugin has no useful concept of stock or shipping class; a store selling a monthly coffee box that way rebuilds the order by hand every month.
The honest warning is about records. A membership plugin that charges outside WooCommerce leaves subscription revenue out of the store's reports and creates two customer records for one person, which costs nothing in month one and a great deal in month eight. Where access is bundled with goods, the cleaner arrangement is the memberships extension in the WooCommerce Marketplace, because it sits on top of orders.
The free option here is real. The free core of Paid Memberships Pro handles the membership lifecycle and recurring charges through the main gateways, which is enough for one level of access and nothing shipped. Check it against the processor in use before committing.
Building on the payment API means owning the state machine
The fourth approach is the subscription API itself: create a customer, create a subscription, store
the identifier, and let webhooks such as invoice.payment_failed and
customer.subscription.updated drive local state. The processor owns the charge attempt,
the card update and the invoice. Everything else is code somebody maintains.
The list is longer than it looks:
- A row per period, with a unique constraint that makes a duplicate impossible rather than improbable.
- Idempotency keys on every write, because the same webhook arrives twice and the second delivery must be a no-op.
- A queue in front of the handler, since granting access inside a request the processor will time out ends with a customer paid and locked out.
- A reconciliation job for invoices the database never saw.
It is right when pricing cannot be expressed as a product: metered usage, per-seat, contracts that
change mid-term. It is wrong for a monthly box. The licence is free and that is the only cheap part;
the cost is the hours of somebody who understands why a duplicate invoice.paid must not
create a second entitlement, and who is awake in month four when the queue stops draining.
When recurring revenue is too small for any of this
Below a few hundred a month in recurring revenue, with an active list short enough to read in one screen, none of the four is worth buying: not the extension, not the hosted layer, not the paid membership tier, and not a custom build. The correct setup is a recurring invoice from the provider, a spreadsheet of next charge dates, a weekly calendar item, and one email template for the failures. That costs a couple of hours a month and has a property the automated versions do not: failures are visible. A store with forty subscribers sees three of them on a spreadsheet; with a plugin, usually none, because nobody configured the alert and a failed renewal counts as churn.
The threshold is not a subscriber count. It is the point where a renewal fails while the person who would have noticed is on holiday, or the manual invoice takes an afternoon. If a subscription is ten percent of revenue, a repeat-purchase reminder usually beats a subscription business with a support burden attached.
The decision rule
- If renewals have to touch the catalogue, with stock, shipping, coupons and tax reports in play, take the official extension. Nothing else keeps the renewal inside WooCommerce, so budget time for monitoring the scheduler.
- If the product renews independently of the catalogue, meaning access or software or hosting, a hosted layer removes the hardest parts and takes the revenue report with it. Make that trade deliberately.
- If the thing sold is access and nothing ships, start with the free core of a membership plugin.
- If no extension can express the pricing, build it, and name the person on call in month four before the first line is written.
- If recurring revenue is a few hundred a month, buy nothing. Invoice manually and revisit when the number moves by an order of magnitude.
- Whichever is chosen, write the invariant down: one charge per subscription per period, keyed so a retry can tell itself apart from a new period. That line survives every migration.
Ana Ferreiro · @aferreiro
WooCommerce engineering
Covers the WooCommerce order lifecycle: checkout, HPOS, session handling, transactional email and the post-purchase layer where most storefront complaints actually originate.
Continue