Cancel, end, retry: stopping a Shopify sale safely
What each stop button on a SafeSale sale actually does: cancel before start, end a live sale, retry a job that gave up, and the shop-wide restore. With the order it happens in.
By Beacon Wave Studio · · 7 min read
Starting a sale gets all the attention. Stopping one is where the mistakes happen, because it is usually done in a hurry: the supplier pulled out, the margin was wrong, the marketing email went to the wrong list, or the sale simply did its job early. At that moment a sale page with four different buttons on it is not reassuring. This is a plain description of what each one in SafeSale does, in the order it does it, so that you press the right one once and do not need to press anything twice.
The four ways a sale stops
A SafeSale sale is a draft until you preview and confirm it, then scheduled until its start time, then active until its end time, then ended. Each of those stages has one exit, and the buttons map onto them:
- Cancel: for a draft or a scheduled sale. Nothing has been written to your store yet.
- End sale now & restore prices: for a live sale. Reverts what the sale wrote.
- Retry now: for a start or end job that failed repeatedly and gave up.
- Restore ALL original prices: on the dashboard, not the sale page. The shop-wide version of end, for when you are not sure which sale is responsible.
There is a fifth button on sales that run an A/B test, Conclude, but that one does not stop the sale; it picks the winner and keeps selling. It is covered in the post on testing discount depth.
Cancel: before anything has been written
When you confirm a sale, SafeSale does two things in your store's data before the start time arrives: it reserves the variants so no other sale can claim them, and it queues a start job for the scheduled time. Prices are not touched. Cancel undoes exactly those two things, in one database transaction: the variant reservations are deleted, any pending jobs for the sale are deleted, the sale is marked Cancelled, and an audit row is written that says "Sale cancelled before it started" with you as the actor.
Because nothing was written to Shopify, there is nothing to restore and no Shopify API call at all. The variants are immediately free for another sale. That also means cancelling is the right move when you realise the targets were wrong: cancel, fix the collection, confirm again. Cancelling is refused for a live sale, with the message "Only draft or scheduled sales can be cancelled; end a live sale instead", so you cannot accidentally leave discounted prices in place by using the wrong exit.
End: the live-sale exit
End is the button people search for at eleven at night. Pressing it does not revert prices in the request itself, which would be slow on a large catalog and would fail halfway if your browser tab closed. Instead it does a short, safe set of writes and hands the real work to the job queue:
- Any pending start job for this sale is deleted, so a sale you end moments before it would have started cannot start afterwards.
- An end job is created to run immediately. If an end job already exists, because the planned end time is close or an earlier attempt is in flight, it is reset to run now rather than duplicated. Pressing the button twice is harmless.
- The sale's end time is set to the current moment and its status becomes Ending.
- An audit row is written: "Restore original prices requested", with the actor who clicked.
Within the next worker tick, which is every thirty seconds, the end job runs. For a sale using Shopify's automatic discount mechanism, the discount is deleted and logged; there are no prices to restore because none were changed. For a price-rewrite sale, every variant in the snapshot is compared against its live price and written back to the original price and compare-at price. Variants that already match their original are counted as skipped rather than rewritten, which is what makes a retry safe. The log then records "Restored N variants to their original prices (M already matched)".
After the prices, the sale's storefront metafields on each product and on the shop are removed, so countdown timers and "was" prices in your theme disappear in the same pass. Finally the variant reservations are released and the sale is marked Ended with a clean error field. If you look at the sale list and see Ending for a minute on a sale with a few thousand variants, that is the restore working through Shopify's rate limits, not a stall.
One detail worth knowing: pressing End on a scheduled sale is redirected to Cancel internally. You get the same clean result either way, and the audit log tells you which one actually happened.
Retry: when a job gave up
Start and end run as jobs in SafeSale's own queue. A job that fails, because Shopify returned an error, a token expired, or the network blinked, is retried with exponential backoff. After eight attempts it is marked dead. Dead jobs are never silently dropped: the sale page shows a red banner titled "start sale gave up" or "end sale gave up" with the last error text, and for an end job the sale itself is flagged Failed so it is obvious in the list. The primary button on a Failed sale changes its label to "Restore original prices".
Retry now resets that job: attempts back to zero, due time now, error cleared, status pending. The next tick picks it up. Nothing else about the sale changes, so retrying is the right first response after you have fixed whatever the error text describes, typically reinstalling or re-authorising the app if the message mentions permissions. Retrying is safe for the same reason ending twice is safe: both apply and restore compare each variant against its snapshot before writing, so a variant that was already handled on attempt three is skipped on attempt four.
If the retried job dies again with the same error, that is a signal the problem is on the Shopify side or in the app's permissions rather than a transient fault, and the shop-wide restore below is the fallback.
Restore ALL: when you are not sure which sale
The dashboard has a button labelled "Restore ALL original prices". It queues a single shop-wide job, with a higher retry budget of twenty attempts, that walks every snapshot the app holds for every sale that could have touched a price, writes the original values back, and deletes every automatic discount the app created. It is idempotent in the same way as a single-sale end, so running it when half the prices are already correct only fixes the other half. If one is already queued, pressing again does nothing rather than queueing a second.
It is also the answer after an uninstall. Shopify revokes an app's API access the moment it is removed, so nothing can be restored from outside; but the snapshots are kept for ninety days, and reinstalling brings back the same button, with no plan required. The public emergency restore guide walks through that order of operations.
Which button, in one table
| Situation | Press | What Shopify sees |
|---|---|---|
| Scheduled sale, wrong targets or date | Cancel | Nothing; no prices were changed |
| Live sale needs to stop now | End sale now & restore prices | Original prices written back, discount deleted |
| Red "gave up" banner on the sale | Fix the cause, then Retry now | The same job runs again, skipping variants already done |
| Prices look wrong and you cannot tell which sale | Restore ALL original prices | Every snapshot restored, every app discount removed |
What none of them do
None of these buttons touch a price the sale did not write. If a staff member changed a variant by hand during the sale, the end job restores that variant to its snapshot original, not to the hand-edited value; the post on prices edited during a live sale goes through that case. None of them delete the snapshot or the audit log, so after the sale is over you can still export the CSV and see what each variant was and became. And none of them are instant in the strict sense: cancel is, because it is a database write, but end and the shop-wide restore are queued and run by the worker, which is the design choice that lets them survive a closed browser tab or a deploy in the middle.
Stopping a sale should be boring. The buttons in SafeSale are arranged so that the one you reach for in a hurry is the one that applies to the sale's current state, and so that reaching for it twice costs nothing.
Frequently asked questions
How do I stop a Shopify sale early and put the prices back?
In SafeSale, open the sale and click "End sale now & restore prices". The sale moves to Ending, the app queues the restore immediately instead of waiting for the planned end time, and every variant goes back to the price and compare-at price recorded in its snapshot. The sale shows Ended once the last variant is written. If you used the discount mechanism rather than price rewrites, the automatic discount is deleted first and there are no prices to restore.
What is the difference between cancelling and ending a sale?
Cancel is for a sale that has not started: a draft or a scheduled sale. Nothing has been written to Shopify yet, so cancelling only releases the variants the sale had reserved and removes its pending start job. End is for a live sale: it reverts the prices or deletes the discount. If you press the end button on a scheduled sale, SafeSale treats it as a cancel.
Why does my sale say a job gave up, and is it safe to retry?
Each start and end runs as a job that retries with increasing delays. After eight failed attempts it is marked dead and the sale page shows the error with a Retry now button. Retrying is safe because both apply and restore compare each variant against its snapshot before writing, so a variant that was already restored on an earlier attempt is skipped, not reverted twice.
Can I undo a sale after it ended?
Ending the sale is the undo: the original prices come back from the snapshot. If something still looks wrong afterwards, the dashboard has a shop-wide "Restore ALL original prices" action that re-reads every snapshot the app holds and writes the originals again. If you have already uninstalled, snapshots are kept for 90 days: reinstall, and the same button is available without a plan.