When a Spryker storefront is slow, the first instinct is to profile PHP. It is almost never the productive place to look. Spryker's architecture already did the big performance move for you — Yves reads denormalised data from Redis and Elasticsearch instead of joining tables at request time — so when things get slow anyway, it is usually because something is working against that design rather than within it. After enough of these engagements, the culprits form a short, repeatable list.

Publish & Sync: fast reads, bought on credit

The reason product pages can be fast is that the work happened earlier: an entity changes in Zed, events fire, publishers rebuild the denormalised view, and sync writes it to Redis and Elasticsearch. That pipeline is the platform's load-bearing wall, and two failure modes account for most of the grief.

The backlog. A full catalog import touches every product, which fires events for every product, which can leave the queues chewing for hours — during which the storefront cheerfully serves stale prices and yesterday's stock. If your nightly import "sometimes isn't finished by morning", this is usually what that sentence means. The fixes are unglamorous: import true deltas instead of the whole feed, batch the events, tune publisher chunk sizes, and put queue depth on a dashboard with an alert. Sync lag is invisible until a customer orders something that sold out yesterday — it deserves a metric more than most things that have one.

The over-eager publisher. A custom publisher that rebuilds too much per event — the classic is touching every abstract product in a category because one attribute changed — multiplies a small edit into thousands of writes. Reading the publisher code with the question "what is the blast radius of one event?" is a profitable afternoon on most mature Spryker projects.

Redis round trips: death by a thousand GETs

Redis answers in well under a millisecond, which tempts people into treating it as free. It is not free from PHP, because each lookup pays the full round trip. A product listing that resolves each tile's data one key at a time — product, then price, then availability, then image sets, then labels — quietly stacks up hundreds of sequential network hops, and the page spends more time waiting on the loop than on any single query.

The remedies are old-fashioned: batch key lookups (MGET) wherever a collection is being hydrated, and treat any per-item storage read inside a foreach as a code smell in review. The storage client's batch methods exist precisely for this; the sin is usually in custom code that never heard of them. The same shape applies to Elasticsearch — one query with facets and the result page, not a query per widget.

While you are in the neighbourhood, look at what is in those keys. I have seen product storage entries north of a hundred kilobytes because a publisher serialised the entire attribute universe "to be safe". You pay to build it, store it, transfer it and unserialise it, on every page, forever. Publish what the frontend reads. Nothing else.

The synchronous call hiding in checkout

Browse and search run on Spryker's fast path. Checkout is where projects smuggle in the slow one: a live ERP credit check, a real-time stock reservation, a tax service, a payment provider's session setup — each a reasonable decision, each adding a synchronous external dependency to the single most conversion-sensitive click in the shop. Three reasonable decisions later, "place order" takes nine seconds and fails whenever any one of three other systems sneezes.

The design questions worth forcing early: which of these answers must be correct at this instant, and which can be seconds stale? Stock can usually be a cached read with a reservation behind the scenes; the credit check often genuinely must be live — in which case it needs a timeout, a fallback policy agreed with finance, and a place in the order state machine for "accepted pending review". What it must never be is an unbounded wait between the customer and their confirmation page.

Measure like a customer, not like a server

The last recurring failure is measuring the wrong number. Server-side response time on a cached product page tells you almost nothing about what the customer felt — front-end weight, third-party tags and cache hit rates routinely dominate. Two habits keep the work honest: track percentiles from real-user monitoring rather than averages from synthetic checks against warm caches, and before any tuning sprint, write down the one journey that matters (usually listing → product → cart → order) with its current p95 numbers. Optimising pages nobody visits is the most popular performance work there is, because it is easier than fixing the import.

None of this is exotic engineering. It is knowing which parts of the machine carry the load, and pointing the effort there instead of at the profiler's prettiest flame graph.