Shopify collection limits
A template language that can't ask for anything runs out so quietly that nothing looks wrong, and once you stop arguing with it the work has to go somewhere else.
A store I was working on had a row of related products along the bottom of every product page. The merchant picked all six cards by hand because they 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 and on the right sales channel. 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. That’s 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. That’s not 20 per loop or 20 per section but 20 on the whole page, counted across everything on it. Ask for a 21st and you get an empty thing back.
The other block on that template was an accessories strip somebody wrote before I got there, doing the same lookup off a metafield. It listed 2 or 3 on a small product and 15 on the expensive ones. And 15 plus my 6 is 21. So on exactly the products bringing in the most revenue, the last card in my row was the one over the line.
Two blocks that had never heard of each other were spending out of one allowance, and the one further down the file lost. Neither of them was 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 comes back as an empty object instead of an error, and that object answers every question with nothing at all.
So related.title renders as an empty string and so does related.url. 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 either because as far as the platform was concerned nothing had gone wrong. It was documented in one line on a reference page I hadn’t 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 keeps going.
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
I spent the rest of the week on something dumber. I kept reading Liquid as a programming language with the good parts taken out. It has loops, conditionals, filters, assignment and a case statement. It looks enough like one to set the expectation.
But it has no way to start anything. 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.
I’ve done Shopify work next to Magento work for about two years now and I still read Liquid with Magento habits. On Magento a template can reach most of the application and routinely does. Figuring out which template was even running there once ate most of a day.
Liquid 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
I’d underweighted what this means for performance. A Liquid template can’t produce an N+1 query because it can’t produce a 1.
I’ve been on projects that lost whole weeks to pages that got slow because a template did a lookup inside a loop. 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 even 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. All of this runs on somebody else’s hardware, next door to a huge number of other people’s stores. A template that could start work could take the machine down with it. 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 thing I actually want out of a hosted platform, seen from the other side. I want one so I don’t have to be the person on the phone at 3 am in the middle of Q4. I’d had those filed as two separate facts about the product. Now I think 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. Every Liquid problem I’ve had since has ended up in one of them.
Put it in the data ahead of time. That means 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, not a product. Keeping those three next to the handle is redundant and a little stale, but anybody can read it. 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. It gets punched into a page that’s 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. If you’ve been arguing with a template for more than a day, that’s a pretty good sign you’re in this case.
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 one that would let me ask a question nobody’s allowed to ask. That’s 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. My first move is still the Magento one, which is to go and get the thing. Two years of Shopify haven’t changed that. Being told that the file I’m sitting in doesn’t get to go and get anything lands as being told I might do it badly.
But it isn’t addressed to me. The constraint is the shape that makes the promise underneath it possible. That promise is worth a lot 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. This version just 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. Maybe that’s the only kind that survives a busy quarter.
The version that 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. I haven’t thought about it since, which is about as close to a review as any of this gets.
One habit stuck. I 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 guess is I’ll have to do it again on the next platform.