I’ve been scanning public storefronts to work out whether inventory health is visible from the outside - no store data, just what any customer can see on a product page. About 65 stores, all independent retailers.
Sharing the negative results, because they surprised me and I haven’t seen them written down anywhere.
Sold-out gaps inside a size run don’t tell you anything. I assumed a run missing its middle sizes was a sign of a store losing track of replenishment. It turned out to be near-universal - I found it almost everywhere, including at stores whose inventory is demonstrably well run. It’s just what retail looks like mid-season. Useless as a signal.
Blank SKUs are probably the opposite of what you’d think. I expected missing SKUs to indicate a messy catalogue. What I actually saw is that clean, complete SKU fields correlate with the stores that are already organised - so the presence of good SKUs tells you more than the absence of them tells you. As a warning sign it’s close to worthless.
What did separate stores, directionally: entire size runs sitting dead (not gaps - whole ranges nobody has touched), and duplicate SKUs, which in the worst cases ran into the hundreds on a single catalogue. Both are visible from outside, and both point at the same thing - the catalogue and the replenishment loop have drifted apart.
Honest limit on all of this: my “clean” comparison is a single store I know well. One store is not a control group, and they may simply buy better than everyone else. So treat it as a hypothesis someone should shoot at rather than a finding.
Curious whether this matches what other people see from inside their own catalogues.
(Disclosure: I build a receiving and barcode-label app, which is why I was looking at this in the first place. Nothing above depends on using it.)
This is a genuinely useful negative result. From a CRO angle, I’d be careful treating a dead size run as a problem until there is demand evidence. The stronger signal is shoppers selecting unavailable sizes, reopening the selector, hitting notify-me, or leaving the PDP.
Catalog state is observable; lost demand is still a hypothesis until behavior supports it. Duplicate SKUs feel more operationally actionable because they can create availability and reporting errors even before conversion moves.
Fair, and I think you’re right — that’s the sharpest hole in it.
The method is external-only by design (I wanted signals visible without asking a merchant for access) and that’s also its ceiling. Catalogue state is all I can see, so “dead run” really means “dead run and nobody has reordered into it” — I can’t separate the store that lost track from the store that correctly decided not to rebuy. Your list is exactly the evidence that would split those, and none of it is visible from outside. So I’d downgrade dead runs from “signal” to “question worth asking the merchant,” which is a real demotion.
Agreed on duplicate SKUs being the more actionable of the two, and for the reason you gave — it breaks availability and reporting whether or not anyone was going to buy. It’s also the one merchants tend not to know about: the catalogues where I found them in the hundreds weren’t showing it in the admin without going looking.
Question back, since you see the behavioural side: when a shopper hits notify-me on a size, does that reliably reach whoever does the buying? Most operational failures I’ve seen are a broken handoff rather than missing data, and that one looks like a candidate.
On the notify-me handoff, no, and I think it breaks by construction rather than by neglect.
Shopify has nothing native for back in stock, so the waitlist lives inside whichever app the merchant installed. That app’s job finishes when the item returns and it emails the people waiting. The reorder decision happens somewhere else, a spreadsheet or Stocky or an ERP, and nothing joins the two. So the demand gets recorded accurately and then consulted after the decision it should have informed. Whoever does the buying usually cannot see a waiting count per variant without opening a second tool they do not log into weekly.
Timing makes it worse. Signups cluster in the first days after a size goes out, which is exactly the window where a reorder is still realistic, and the list is longest at the moment nobody is looking at it.
Your duplicate SKU finding lines up with something on the Shopify side. The admin does warn about a duplicate SKU in the inventory section of the product page, but it does not block the save, and CSV import and app or API writes create variants without anyone ever seeing that warning. So duplicates arrive through the paths nobody watches, and the one place they surface is a screen you would have to already suspect. That fits a catalogue carrying hundreds without the merchant knowing.
Stocky goes away after 31 August, which is next week. Were the stores you scanned mostly running a separate replenishment tool, or back to spreadsheets?
Spreadsheets or a separate tool — I can’t tell, and it’s the same ceiling as before: everything I looked at was the storefront, so the buying side is invisible to me. The impression, and it’s only that, is that the worst duplicate counts didn’t look like stores running any dedicated tool — the catalogue had the texture of something edited by several people over several years.
Your point about where duplicates come from is the part I didn’t have. I’d been treating it as an accumulation problem, where the longer a catalogue lives the messier it gets. “They arrive through CSV import and API writes, where the warning never fires” is a mechanism, and it explains something I couldn’t: the counts I saw were lumpy rather than proportional to catalogue age. One bad import can leave a store looking worse than ten years of hand editing.
It matches something at the other end of the flow, too. Adding scan-to-receive, the case that took longest to get right was two products sharing a barcode — it ended up stopping to ask rather than picking the likelier line, because a scanner will commit the wrong one at speed and nobody notices until a count is off. Same duplicate, different moment.
The timing point is the one I keep coming back to. If signups cluster in the first days after a size goes out, and that’s the same window where reordering is still realistic, the data isn’t just siloed — it’s siloed exactly when it would have been worth something.
The waitlist never reaching the buyer is half the damage. The quieter half is that the stockout corrupts the number the next buying decision runs on.
Every reorder calculation starts from sales per day over some window. Days at zero stock count as zero demand days. So the average drops in exact proportion to how often the item is gone, and the items gone most often are your best sellers, which means the products that most deserve a deeper order are the ones the math keeps trimming. Under-bought because they sold out. Which sells them out again. The loop looks like a buying mistake and it’s really an arithmetic one.
The fix is small. Compute velocity over the days the item was actually in stock, not the calendar. Thirty sold across twenty available days is 1.5 a day, not 1, and on a product that’s out a third of the time that gap compounds into a permanently thin order every cycle.
Which is where the notify-me list earns its place in this study. Signups during the gap measure demand directly while sales read zero. A floor, not a total, since most shoppers leave without signing up. But it splits the two stores that look identical from outside: a dead size run with a long waitlist behind it is a store that lost track, and a dead run with no waitlist may be a store that correctly stopped rebuying. That’s the demand evidence the external view can’t see. I think it’s exactly the ceiling the study ran into.
you’ve named the ceiling more precisely than I did. I said I can’t see demand from outside; what you’re describing is worse than that. The demand isn’t just invisible, it’s actively mis-recorded inside, and mis-recorded in the direction that penalises the best sellers hardest. A store can be doing everything right procedurally and still ratchet its own orders down.
The velocity fix is arithmetically trivial and I suspect organisationally hard, for one reason: you need days-in-stock, and I don’t think that’s recoverable after the fact. Shopify holds the current inventory level, not a history of it, so unless a merchant has been logging levels or had an app doing it, “30 sold over 20 available days” isn’t a number they can go back and compute for last season. Happy to be told I’m wrong — that’s the kind of thing where someone here will know a source I don’t.
If it is right, the practical version is depressing but actionable: you can’t fix last year’s buy, you can only start recording now. Which does mean it starts at the receiving end — the moment stock arrives is the one timestamp everyone already captures, and it’s half of what you need to reconstruct availability going forward.
And the waitlist split is the cleanest version of what Icey.Lane raised at the top. Dead run with a waitlist means lost track; dead run without one may be a correct decision. That’s the test I’d want, and it’s not one you can run from outside — which probably is the hard limit on how far the external view goes.
Hey there, these are some interesting findings. Shopify doesn’t give you a simple store-wide duplicate check, so duplicate SKUs can easily go unnoticed.
A practical fix is to export your product CSV regularly and check the SKU column for duplicates. It only takes a few minutes and can catch issues before they become a bigger catalogue problem.
Also, one thing I’d add is checking product update activity over time, for example, products that have remained unchanged for months while still being active.
That could help distinguish normal mid-season stock gaps from genuinely neglected inventory, especially in larger catalogues.
We have a stocktaking app that does these checks along with a few more. Same as your barcoding app, our app does not depend on all of these but flagging product data issues seemed like a useful feature. Duplicate or missing SKUs/barcodes is a problem when counting stock.
The update-activity idea is the most useful new suggestion in here for the method I was actually using, because I think it’s one of the few things on the list that’s testable from outside. updated_at is in the public products.json, so “active but untouched for months” is computable without any store access — unlike the waitlist signal, which everyone agrees would be decisive and nobody outside can see.
One confound I’d want to handle first: app and theme writes bump updated_at without anyone making a merchandising decision. A bulk price sync, a tag update, an app writing metafields — all of that looks like activity. So the useful version is probably “unchanged on the fields that matter” rather than the record as a whole, and field-level history isn’t visible from outside either. Which lands me back at a proxy.
Still worth running. If it separates the stores I flagged from the ones I didn’t, it’s the first thing anyone has suggested that survives the external view.
On the CSV duplicate check — agreed that’s the practical fix. I suspect the reason duplicates reach the hundreds is precisely that it’s a manual habit nobody keeps up. It costs nothing right up until the point where it’s a four-figure number.
That’s the inside view of what I was trying to infer from outside, and it’s worse than I’d guessed. 1,593 duplicate SKUs and 1,674 lines missing cost of goods on one catalogue is exactly the kind of thing that never announces itself anywhere in the admin.
The missing cost of goods count is the one I’d point people at. It connects to what Kalvin_Mizzi said further up about reorder maths — if cost is absent on that many lines, margin and velocity reporting are quietly wrong across a large share of the catalogue, and nothing surfaces it until someone goes looking.
A question, since you can see this from inside and I can’t: when you run those checks across stores, do the duplicate SKUs cluster within products — variants of the same product sharing a code — or are they spread across unrelated products? Tobi.Akilo asked me the same thing upthread and I had to admit I hadn’t recorded it.
You can get most of it back from order timestamps, which every store still has.
A product that sells steadily, goes quiet for twelve days, then starts selling again within an hour of a receiving event wasn’t resting. It was out. Real demand pauses don’t line up with your own PO arrivals, so the silent gaps that end at a restock are your stockout days, and available days is just the calendar minus those. Conservative, misses the short outages, ambiguous on slow movers. On best sellers it’s not ambiguous at all, and those are the ones the corrected number moves most.
Your receiving instinct is the same idea from the other side. Arrival stamps the left edge of every in-stock window, the last sale before a silence stamps the right. Between the two you can rebuild last season closer than you’d think.
One smaller correction: the admin does keep a per-variant inventory history, but roughly 90 days and one variant at a time. Helps audit the recent buys, not last year’s.
Corrected, and thank you — I was flatly wrong that there’s no history. Ninety days, per variant, one at a time is exactly the shape that makes it useless for a season review and perfectly fine for auditing last month’s buys. I collapsed a real distinction there.
The reconstruction from order timestamps is better than what I was reaching for, because it needs no history at all — just the two things every store keeps. And it already handles the confound I’d have raised: a product can be silent because it was unpublished or mispriced rather than out of stock, but silence that ends at a receiving event rules that out. An unpublished product doesn’t start selling because a box arrived.
Two places it breaks, both on the left edge, and both from my side of the fence:
Partial receipts. If a PO lands in two shipments three weeks apart there isn’t one arrival stamp, there are two, and the first may not have been enough to end the silence. The receipt line for that variant is the timestamp you want, not the PO date.
Multi-location. A restock at the warehouse ends the silence for online while the shop floor is still empty. Available days genuinely differ per location, so one store-wide number will overstate availability for whichever location didn’t get the stock.
Neither is fatal, and both narrow the same way you already narrowed it — toward best sellers at a single location, where it isn’t ambiguous. But if anyone builds this, use the receipt line rather than the PO.
You don’t have to pick which receipt. The resumption picks it. The receipt lines are the candidate set, and whichever one the sales restart after is the one that counted, so a first shipment too small to end the silence just means the variant was still out through it. Which is the right answer anyway. The partial case collapses back into the ordinary one as long as you key on the gap closing and not on a receiving event happening.
Multi-location I’d call a partition rather than a break. Fulfillments record which location shipped, so you can run the same gap detection per location and come out with an availability calendar for each one.
What actually spoils that is routing. An online order placed near the empty shop floor gets fulfilled out of the warehouse, so the warehouse carries the demand in the data and the floor reads dead when it’s only empty. POS is clean, since the location is the location. Online you’re measuring where the stock was rather than where the demand was, and on two locations that’s close enough to use. Past that I’d stop trusting it.
A caution on the updated_at idea before you build on it, because from the app side I can tell you it measures something else. Installed apps rewrite products constantly - feed tools, SEO apps, sync jobs, us included. Our own hourly job touches products with zero human involvement, and every touch moves updated_at. So “active but untouched for months” from the outside mostly means “no app installed”, and a store that looks freshly tended may not have had a human in the admin since spring. The signal isn’t attention, it’s automation count.
If you rerun the study, the inverse read is more reliable: a catalog where updated_at is months old across the board genuinely has nobody and nothing touching it. Stale is trustworthy; fresh is ambiguous.
On duplicate SKUs - Tobi asked if there’s an innocent reason, and I can add a mechanical data point. We match products between two stores a merchant owns, SKU first, then barcode, then handle. Duplicate SKUs are the single most common reason matching turns ambiguous, and in every case we’ve traced with a merchant it came down to years of hand-editing by different people, exactly the texture Roberto described. Never a deliberate scheme, always sediment.
Disclosure: that matcher is StoreTwin, my app - the sample is small and skewed to stores that install a sync tool, so weigh it accordingly.
Both fair, and the partial-receipt one collapses exactly as you say. I was keying on the receiving event when the gap closing is the actual signal — if a first shipment was too small to restart sales then the variant was still out through it, and the method has already recorded that correctly. I was objecting to a design nobody proposed.
Routing is the better problem and I hadn’t seen it. One thing from the receiving side with the same shape: a transfer between locations stamps the left edge at the destination exactly like a supplier delivery does. For a per-location availability calendar that’s fine — either one genuinely ends a local stockout. But if the same reconstruction is used to reason about supplier lead times, transfers will quietly shorten them, because stock appeared at B without anyone having ordered anything. Worth separating the two event types even where, for availability, they’re interchangeable.
That’s a better objection than the one I raised, and it kills the version I had in mind. I’d worried about bulk writes, which leave a detectable spike in a histogram. A continuous hourly job doesn’t spike — it just floors the metric for the whole store. “Automation count rather than attention” is exactly it.
The inversion is the salvage, and it changes the shape of the claim: it becomes a one-sided test. Stale across the board identifies a neglected catalogue with reasonable confidence; fresh tells you nothing, because it could be a person or a feed tool. Still worth having, but half of what Smruti-MONK and I were both assuming. When I rerun it I’ll report only the stale end.
And thank you for the duplicate-SKU answer — Tobi asked me that upthread and I couldn’t give one. “Never a deliberate scheme, always sediment” is a better phrasing of what I was gesturing at, and it carries more weight coming from matching data than from my impression. That it’s the single most common reason your matcher turns ambiguous is the strongest evidence in this thread that it’s operational rather than cosmetic.