Shopify collection limits
On a template language that can't ask for anything, why running out looks exactly like nothing being wrong, and where the work goes once you stop arguing.
A store I was working on had a row of related products along the bottom of every product page. Six cards, chosen by hand, because the merchant had firm views about what goes with what and no interest in an algorithm having them instead. On most products it looked exactly the way it was drawn. On maybe a third of them, one card was a blank space.
The handles were spelled right. The products were published, in stock, on the right sales channel, and pasting a handle into the address bar brought the page up fine. So the card was empty in one place and the product was healthy everywhere else, which is the flavor of bug where you start distrusting the browser instead of yourself.
The section was doing the ordinary thing. It read the merchant’s list off a setting, split it, and looked each handle up.
{% assign handles = section.settings.related | split: ',' %}
{% for handle in handles %}
{% assign related = all_products[handle | strip] %}
<a class="related-card" href="{{ related.url }}">
<img src="{{ related.featured_image | img_url: '300x' }}" alt="{{ related.featured_image.alt }}">
{{ related.title }}
</a>
{% endfor %}
all_products hands you 20 products per page render. Not 20 per loop, not 20 per section. 20 on the page, counted across everything on it, and if you ask for a 21st you get an empty thing back.
The other block on that template was an accessories strip somebody wrote before I got there, doing the identical lookup off a metafield. On a small product it listed 2 or 3. On the expensive ones it listed 15. 15 plus my 6 is 21, so on exactly the products carrying the most revenue, the last card in my row was the one over the line.
Two blocks that had never heard of each other, spending out of one allowance, and the one further down the file loses. Neither of them is wrong. You can’t find that by reading either one.
Nothing is a value
The silence cost me a day. Liquid doesn’t raise. A product it can’t give you isn’t an error, it’s an empty object that answers every question with nothing at all, so related.title renders as an empty string and so does related.url, and an anchor with an empty href points at the page you’re already on.
<a class="related-card" href="">
<img src="" alt="">
</a>
The merchant saw a card with no picture and no words on it that reloaded the page when they clicked it. Nothing in the theme editor said anything. Nothing in the logs said anything, because as far as the platform was concerned nothing had gone wrong. It was documented, in one line, in a reference page I had not read lol
I took that as sloppiness for a while and I’ve come around on it. These files get edited by people who aren’t developers, on a live store, at 11 pm, with the whole business running through them. A mistyped handle that took the page down would be a much worse product than one that renders the other five cards and carries on. Failing quiet is the right call for a platform like this, tbh. It also means the page will lie to you with a completely straight face.
It isn’t a small programming language
The rest of the week went on something dumber, which is that I kept reading Liquid as a programming language with the good parts removed. It has loops, conditionals, filters, assignment, a case statement. It looks enough like one to set the expectation.
It hasn’t got any way to start something. No network, no filesystem, no database, no importing anything, no defining a function and calling it later, no recursion. There’s no sentence you can write in a .liquid file that goes and gets a thing the platform hadn’t already decided to hand you before rendering began. So the 20 isn’t a rule bolted onto a language. It’s the shape of the thing showing through the paint. Everything the page can see was fetched in advance, and all_products is a small window onto the part of the catalog that wasn’t.
Coming off Magento, where a template could reach the entire application and routinely did, and where figuring out which template was even running once ate most of a day, that took some getting used to. It isn’t a language you program in. It’s a form with fields on it, and the fields are all there is.
You can’t get an N+1 without a 1
The consequence I’d underweighted is the performance one. A Liquid template can’t produce an N+1 query, because it can’t produce a 1. I’ve spent whole weeks of my life on pages that got slow because a template did a lookup inside a loop, and on the worst of them the fix in the end was putting a cache in front of the entire store. That whole category of problem doesn’t exist here. It can’t be written down.
The rest of the limits are the same idea aimed at time and memory: a cap on how many products you can touch, a cap on how deep you can nest, a clock the render has to finish inside. This is running on somebody else’s hardware, next door to a very large number of other people’s stores. A template that could start work could take the machine down with it, and one that could go and read something could go and read the store next door.
Which means the restriction I’d spent days shoving against is the same object as the thing I actually want out of a hosted platform, seen from the other side, and the thing I want is not being the person on the phone at 3 am in the middle of Q4. I had those filed as two separate facts about the product. My sense now is that there’s only one of them.
Four places for the work to go
Once I stopped trying to win the argument, there were four places for that work to go, and every Liquid problem I’ve had since has been one of them.
Put it in the data ahead of time. A metafield on the product, written by whatever process actually knows the answer, on whatever schedule the answer changes. This is right far more often than it feels like in the moment, because most of what I wanted to compute per request was stable for days and had no business being worked out again for every visitor. It’s the same conversation as adding a field to a product on a platform that owns its own database, minus the four days.
Denormalize what you actually render. My cards needed a title, an image and a URL. They did not need a product. Keeping those three next to the handle is redundant and slightly stale and entirely legible. It costs nothing out of an allowance the page is already spending somewhere else.
Move it to the browser. The rendered page can still fetch, so if the thing genuinely varies per visitor then that’s where it lives, punched into a page that is otherwise identical for everybody, for the same reason you’d do it in front of a cache.
Move it off the platform. A small service does the work on its own schedule and writes the result back through the API. That’s the honest answer when the requirement really is computational, and the tell that you’re in this case is that you’ve been arguing with a template for more than a day.
I lost
I spent about a week on this and maybe a day of it was the bug. The rest was me going back through the objects reference looking for the one that would let me ask a question nobody is allowed to ask, which is less a needle in a haystack than a needle nobody ever put in the barn.
It took that long, I think, because a constraint like this arrives sounding like a comment on your ability. I could write that query in my sleep and have, oodles of times, on the other platform. Being told that the file I’m sitting in doesn’t get to have queries lands as being told I might do it badly. But it isn’t addressed to me. It’s the shape that makes the promise underneath it possible, and that promise is worth a great deal more to the businesses I build for than any trick I might have pulled at render time is worth to anybody, me included.
It’s the same trade as a number in a document that the site is now more than twice over, or a browser matrix nobody gets around to signing, except this version has teeth. The ones we write down need everybody to remember to be good at the end of a long week. The one in the platform doesn’t ask anybody to remember anything, which I suspect is the only kind that survives a busy quarter.
What went out in the end reads one list off a metafield, renders whatever’s in it, and stops at a count the page can afford. It’s a lot plainer than what I spent that week trying to build and I haven’t thought about it since, which is about as close to a review as any of this gets. The habit I kept is that I go and read a platform’s limits now before I design anything on top of them. I try to read them as a list of things nobody on here can do to me instead of a list of things it won’t let me do. It took me a week to get there and my honest guess is I’ll have to do it again on the next platform.