28.09.2026・TechStuff
28.09.2026・TechStuff

Shopware 6 Hidden Gems #23: Why your sitemap silently stopped updating (and sitemap:generate –force)

Fabian Blechschmidt

A slow-burn bug report, verbatim from memory: „Google Search Console shows our sitemap is three months old. New categories aren’t in it.“ Three months. No errors in the log worth noticing, the scheduled task looks healthy, and regenerating „worked“ — or seemed to.

The cause turned out to be a mechanism that’s completely reasonable and completely undocumented: the sitemap generator locks itself, and the lock can outlive a crashed generation.

The lock

When sitemap generation starts, SitemapExporter sets a lock so two processes don’t generate the same sitemap concurrently (Content/Sitemap/Service/SitemapExporter.php:81-91):

private function lock(SalesChannelContext $salesChannelContext, bool $force): void
{
    $key = $this->generateCacheKeyForSalesChannel($salesChannelContext);
    $item = $this->cache->getItem($key);
    if ($item->isHit() && !$force) {
        throw SitemapException::sitemapAlreadyLocked($salesChannelContext);
    }

    $item->set(true);
    $this->cache->save($item);
}

Look closely at what that lock is: a plain cache item, one per sales channel + language, deleted only when generation finishes cleanly (unlock(), line 93). There is no TTL on the lock, no heartbeat, no „held by PID“ metadata.

Now the failure mode writes itself: generation gets killed mid-run — OOM on a huge catalog, a deploy restarting the worker, a Kubernetes pod eviction, someone’s Ctrl+C. The unlock() never runs. The cache item stays. And from that moment, every future generation attempt — scheduled task included — throws AlreadyLockedException, gets logged as a warning at best, and your sitemap is frozen in time. The scheduled task keeps „running successfully“ in the sense that it successfully determines it can’t run.

The fix

That’s what the undocumented --force flag is for:

bin/console sitemap:generate --force

It skips the lock check (and sets a fresh lock for its own run). One command, sitemap unfrozen. The -i/--salesChannelId option limits the run to one sales channel if only one is stuck — the command otherwise loops over every sales channel and every domain language combination (SitemapGenerateCommand.php:79-90).

Since the lock is just a cache item, a full cache:clear also releases it as a side effect — which explains the most confusing symptom variant: „the sitemap fixed itself after our last deployment.“ No, your deployment cleared the cache and with it the stale lock.

Monitoring the real signal

The operational takeaway is bigger than the flag: „sitemap generation ran“ and „a sitemap was generated“ are different facts. What we monitor since:

And if you wrap sitemap generation in your own deployment or cron tooling, prefer --force in scheduled contexts where you know no parallel generation can exist — a stale lock then can’t strand you for months.

TL;DR

Sitemap generation guards itself with a per-sales-channel cache lock that has no TTL and only clears on clean completion. A killed generation strands the lock, and every following run — including the scheduled task — silently refuses with AlreadyLockedException until a cache:clear or sitemap:generate --force breaks the spell. Monitor sitemap file age, not task exit codes. Both the flag and the lock behavior: undocumented.

Next up: Feature::silent() and friends had their moment in part 4 — so instead: plugin:list --json and building CI checks on plugin state.

Had a frozen sitemap? Drop me an email with how long it took to notice 😀


Found in Shopware 6.7.0.0: lock/unlock in Content/Sitemap/Service/SitemapExporter.php:49,67,81-96 (cache item, no TTL), --force and --salesChannelId in Content/Sitemap/Commands/SitemapGenerateCommand.php:50-58, per-channel-per-language loop at 79-90.