WooCommerce mağazası "yavaş" dendiğinde çoğu zaman tek bir sorundan söz etmiyoruz. Ana sayfa ve kategori sayfaları anonim ziyaretçiye tam sayfa önbelleğinden (full-page cache) hızla gelir; sepet, ödeme (checkout) ve hesabım sayfaları, sepete ekleme istekleri ve yönetim paneli ise her seferinde PHP ve veritabanında baştan hesaplanır. Mağazayı asıl yavaşlatan bu önbelleğe girmeyen yoldur, hızlandırma planı da buradan başlamalıdır. Bu rehberde önce neyin yavaş olduğunu ölçüyor, ardından sunucu boyutlandırma, PHP-FPM, veritabanı ve yüksek performanslı sipariş depolama (High-Performance Order Storage, HPOS), önbellek kuralları, zamanlanmış görevler (Action Scheduler) ve ön yüz konularını öncelik sırasıyla ele alıyoruz. Ayar adlarını ve komutları WooCommerce 11.2.1 ve onunla gelen Action Scheduler 4.1.0 üzerinde kontrol ettik; eski sürümlerde bazı seçenekler bulunmayabilir. Genel WordPress hızlandırma konularına yalnızca WooCommerce'i doğrudan etkilediği yerde değiniyoruz. Platform kararını henüz vermediyseniz önce Shopify, WooCommerce, Magento ve özel yazılım karşılaştırmamıza göz atın.
1. Önce ölçün: "yavaş" tam olarak hangi istek?
Herhangi bir ayarı değiştirmeden önce aşağıdaki ölçümleri alın ve bir tabloya yazın. Her değişiklikten sonra aynı ölçümleri aynı ürünle ve benzer saatlerde tekrarlayın; aksi hâlde hangi adımın işe yaradığını ayırt edemezsiniz.| Ölçüm | Nasıl | Neye bakılır |
|---|---|---|
| Önbellekli sayfa (kategori, anonim ziyaretçi) | curl, tarayıcının geliştirici araçları | Yanıt başlığında önbellek isabeti (HIT) ve ilk bayta kadar geçen süre (TTFB) |
| Önbelleksiz ürün sayfası | Benzersiz bir sorgu dizesiyle curl | PHP ve veritabanının saf maliyeti. web.dev, TTFB için kaba hedef olarak 0,8 saniye ve altını önerir |
| Sepete ekleme | Geliştirici araçları > Network: ?wc-ajax=add_to_cart ya da Store API isteği | İsteğin süresi ve ardından gelen ek AJAX çağrıları |
| Ödeme sayfası ve sipariş onayı | Network: ?wc-ajax=update_order_review ve ?wc-ajax=checkout (klasik) ya da /wp-json/wc/store/v1/checkout (blok) | Kargo, vergi ve ödeme sağlayıcısı API'lerinin bekletmesi burada görünür |
| Yönetim: sipariş listesi ve sipariş düzenleme | Query Monitor | Sorgu sayısı, en yavaş sorgu ve onu üreten eklenti |
| Arka plan işleri | WooCommerce > Durum > Zamanlanmış eylemler | Bekleyen, süresi geçmiş (past-due) ve başarısız eylem sayıları |
Bash:
# Önbellekli sayfa: TTFB ve toplam süre
curl -s -o /dev/null -w "TTFB %{time_starttransfer}s toplam %{time_total}s\n" https://magaza.example.com/urun-kategori/ayakkabi/
# Önbellek isabet etti mi? (başlık adı sunucuya ve CDN'e göre değişir)
curl -sI https://magaza.example.com/urun-kategori/ayakkabi/ | grep -iE "x-cache|x-litespeed-cache|cf-cache-status|^age|cache-control"
# Önbelleksiz ölçüm: benzersiz bir sorgu dizesi çoğu önbellekte ıskalamaya (MISS) yol açar
curl -s -o /dev/null -w "TTFB %{time_starttransfer}s\n" "https://magaza.example.com/urun/ornek-urun/?olcum=$(date +%s)"
Ölçüm için kullanacağınız araçlar:
- Query Monitor: Her sayfanın veritabanı sorgularını eklenti ve tema bazında gruplar; yavaş, tekrarlanan ve hatalı sorguları, kanca (hook) çağrılarını ve dış HTTP isteklerini süreleriyle gösterir. Ödeme sırasında kargo ya da ödeme API'sinin kaç saniye beklettiğini en hızlı buradan görürsünüz. Ölçümü bitirince devre dışı bırakın.
- Yavaş sorgu günlüğü (slow query log): Tek bir sayfada görünmeyen ama gün boyu biriken ağır sorguları yakalar. Özeti mysqldumpslow (MariaDB'de mariadb-dumpslow) ya da pt-query-digest ile çıkarın.
- PHP-FPM durum sayfası: Boşta ve meşgul süreçleri, bekleme kuyruğunu (listen queue) ve "max children reached" sayacını gösterir; kurulumu 3. bölümde.
- Zamanlanmış eylemler ekranı: Action Scheduler kuyruğunun sağlığını gösterir; ayrıntısı 6. bölümde.
- APM (isteğe bağlı): New Relic, Datadog ya da Tideways gibi bir uygulama performans izleme aracı yavaş işlemleri fonksiyon düzeyinde ve kalıcı olarak izler; kampanya günlerindeki dalgalanmayı geriye dönük görmenizi sağlar.
INI:
# /etc/mysql/conf.d/slow.cnf (yol dağıtıma göre değişir); ardından servisi yeniden başlatın
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
long_query_time değerinin varsayılanı 10 saniyedir; bir mağaza için 1 saniye daha anlamlı bir başlangıçtır. Birkaç günlük kayıt topladıktan sonra eşiği düşürebilirsiniz.
2. Sunucu boyutlandırma: çekirdek, RAM ve PHP worker sayısı
Anonim katalog trafiği önbellekten karşılandığı sürece sunucunun yükünü önbelleğe girmeyen istekler belirler: sepet, ödeme, hesabım, ?wc-ajax= çağrıları, Store API (/wp-json/wc/store/...), giriş yapmış müşteriler ve yönetim paneli. Bu isteklerin her biri bir PHP-FPM sürecini (worker) meşgul eder. Gereken eşzamanlı worker sayısını Little yasasıyla kabaca hesaplayabilirsiniz:Eşzamanlı istek ≈ saniyedeki önbellek dışı istek × ortalama yanıt süresi (saniye)
Örnek: Kampanya saatinde saniyede 6 önbellek dışı istek geliyor ve bu istekler ortalama 0,5 saniyede tamamlanıyorsa ortalamada 3 worker meşguldür. İstekler saniyelere eşit dağılmadığı için 3-4 katı pay bırakıp 9-12 worker hedeflemek makul bir başlangıçtır. Bu sayılar bir ölçüm sonucu değil, yöntemi gösteren bir örnektir; kendi sitenizde istek sayısını erişim günlüğünden (access log), yanıt süresini yukarıdaki ölçümlerden alın.
CPU: Bir PHP isteği tek iş parçacığında çalışır; işlemci üzerinde çalıştığı anlarda en fazla bir çekirdek kullanır. İstekler zamanlarının çoğunu veritabanını ya da dış bir API'yi bekleyerek geçiriyorsa çekirdek sayısından fazla worker işe yarar; tamamen CPU'da geçiyorsa fazladan worker yalnızca sıra oluşturur ve her isteği yavaşlatır. Worker sayısını artırdıktan sonra CPU kullanımına ve yanıt sürelerine birlikte bakın: CPU tavana dayanmış ve yanıt süreleri uzuyorsa çözüm daha fazla worker değil, daha fazla çekirdek ya da daha hafif koddur.
RAM: Bütçeyi şu sırayla kurun: işletim sistemi ve web sunucusu, InnoDB arabellek havuzu (buffer pool), Redis ve kalan kısım PHP-FPM. PHP'nin memory_limit değeri istek başına bir tavandır, her worker'ın gerçekten kullandığı bellek değildir; boyutlandırmada worker'ların ölçülen bellek kullanımını (RSS) esas alın:
Bash:
# Çalışan php-fpm süreçlerinin ortalama RSS değeri (ana süreç de sayılır; kaba tahmin için yeterli)
ps -eo rss,comm | awk '/php-fpm/ {s+=$1; n++} END {printf "%d süreç, ortalama %.0f MB\n", n, s/n/1024}'
Aşağıdaki tablo, tek sunucuda Nginx, PHP-FPM, MariaDB ve Redis çalıştığı varsayımıyla hazırlanmış bir hesap örneğidir; değerler ölçüm değil, varsayımdır:
| Kalem | 4 vCPU, 8 GB RAM | 8 vCPU, 16 GB RAM |
|---|---|---|
| İşletim sistemi, Nginx, izleme ajanı | 1 GB | 1,5 GB |
| InnoDB arabellek havuzu (veri ve indeks boyutuna göre) | 2 GB | 5 GB |
| Redis (maxmemory) | 512 MB | 1 GB |
| PHP-FPM'e kalan | yaklaşık 4,5 GB | yaklaşık 8,5 GB |
| Worker başına 120 MB varsayımıyla bellek sınırı | en fazla yaklaşık 38 worker | en fazla yaklaşık 72 worker |
Tablodaki sayı yalnızca bellek tavanıdır: 4 çekirdekte 38 worker'ın aynı anda CPU isteyen iş yapması çekirdekleri boğar. pm.max_children değerini bellek tavanı ile CPU gözleminden hangisi küçükse ona göre seçin, ardından PHP-FPM durum sayfasındaki kuyruğa bakarak ince ayar yapın.
Paylaşımlı hostingden çıkma zamanının geldiğini gösteren işaretler:
- CloudLinux tabanlı paylaşımlı hostingde 508 hataları: Eşzamanlı giriş süreci (entry processes) sınırı dolmuştur. Bellek ya da süreç sınırına takılınca 500 veya 503, CPU ya da disk G/Ç sınırına takılınca yavaşlama görürsünüz.
- Ödeme sayfasının kampanya saatlerinde zaman aşımına uğraması, sakin saatlerde ise sorunsuz çalışması.
- Gerçek bir sistem cron'u, SSH ve WP-CLI, Redis ya da PHP-FPM ayarlarına erişememek: Bu rehberdeki adımların çoğunu uygulayamazsınız.
- Zamanlanmış eylemlerde süresi geçmiş iş birikmesi ve oturum tablosunun durmadan büyümesi.
3. PHP sürümü, OPcache ve PHP-FPM
WooCommerce'in sunucu önerileri sayfası (10.8 ve sonrası için) PHP 8.3 veya üstünü öneriyor ve bu yazı hazırlanırken PHP 8.4'e kadar test edildiğini belirtiyor; listede MySQL 8.0+ ya da MariaDB 10.6+, HTTPS ve en az 256 MB WordPress bellek sınırı da var. Sayfa, PHP 7.4 ve MySQL 5.6 gibi eski sürümlerde de çalıştığını ama bunların ömrünü doldurduğunu not ediyor. php.net'e göre PHP 8.2'nin güvenlik desteği 31 Aralık 2026'da, PHP 8.3'ünkü 31 Aralık 2027'de bitiyor. Sürüm yükseltmesini önce hazırlık ortamında (staging) deneyin ve özellikle ödeme ile kargo eklentilerini test edin.OPcache: PHP dosyalarının derlenmiş hâlini bellekte tutar; kapalıysa her dosya, dolmuşsa önbelleğe sığmayan dosyalar her istekte yeniden derlenir. Varsayılan değerler (128 MB bellek, 8 MB interned string arabelleği, 10000 dosya) bir mağaza için dar kalabilir: Yalnızca WooCommerce 11.2.1'in kurulum paketinde 3.600'den fazla PHP dosyası var; WordPress çekirdeği, tema ve diğer eklentilerle bu sayı hızla büyür.
INI:
; /etc/php/8.3/fpm/conf.d/10-opcache.ini (Debian/Ubuntu yolu; dağıtıma göre değişir)
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
; Eklentiler yönetim panelinden güncelleniyorsa zaman damgası kontrolünü açık bırakın
opcache.validate_timestamps=1
opcache.revalidate_freq=60
opcache.validate_timestamps=0 dosya kontrolünü tamamen kaldırır; ancak o zaman panelden yapılan eklenti güncellemeleri PHP-FPM yeniden yüklenene kadar devreye girmez. Bu ayarı yalnızca dağıtımı (deploy) bir betikle yapıp ardından PHP-FPM'i yeniden yüklüyorsanız kullanın. revalidate_freq=60, değişen bir dosyanın en geç bir dakika içinde fark edilmesi demektir. JIT'e öncelik vermeyin: PHP 8.0 sürüm notları, JIT'in yapay testlerde büyük kazanç sağlarken tipik uygulamalarda PHP 7.4 düzeyinde kaldığını söylüyor.
PHP-FPM süreç yöneticisi: pm = static, sabit sayıda worker'ı baştan açar; bellek kullanımı öngörülebilir olur ve ani yükte süreç başlatma gecikmesi yaşanmaz. Mağazaya ayrılmış bir sunucuda en sade seçenek budur. pm = dynamic, boştaki worker sayısını alt ve üst yedek değerleri arasında tutar; sunucuda başka işler de çalışıyorsa uygundur. pm = ondemand, worker'ı istek gelince açar, boşta kalınca kapatır; düşük trafikli sitelerde bellek kazandırır ama trafik sıçradığında ilk istekler bekler.
INI:
; /etc/php/8.3/fpm/pool.d/www.conf (Debian/Ubuntu yolu)
pm = static
pm.max_children = 16 ; bellek bütçesi ve CPU gözlemine göre
pm.max_requests = 500 ; worker'ı 500 istekte bir yeniler, yavaş bellek sızıntılarını sınırlar
; Sunucuda başka işler de varsa static yerine:
; pm = dynamic
; pm.start_servers = 4
; pm.min_spare_servers = 2
; pm.max_spare_servers = 6
pm.status_path = /fpm-status
request_slowlog_timeout = 5s
slowlog = /var/log/php8.3-fpm-slow.log
request_terminate_timeout = 100s
request_slowlog_timeout, 5 saniyeyi aşan isteklerin PHP yığın izini (backtrace) slowlog dosyasına yazar; ödemenin hangi eklenti fonksiyonunda takıldığını çoğu zaman buradan bulursunuz. Durum sayfası istek adreslerini ve kaynak bilgisini gösterdiği için onu yalnızca sunucunun kendisine açın. Aşağıdaki bloğu yerelden erişilen (örneğin 127.0.0.1'i dinleyen) bir server bloğuna ekleyin:
Code:
# Nginx: yalnızca yerelden erişilen PHP-FPM durum sayfası (soket yolu dağıtıma göre değişir)
location = /fpm-status {
allow 127.0.0.1;
deny all;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
# Okumak için: curl -s "http://127.0.0.1/fpm-status?full"
Durum sayfasında listen queue sürekli sıfırın üzerindeyse ve max children reached sayacı artıyorsa istekler boş worker bekliyordur. CPU boştaysa pm.max_children değerini artırın; CPU doluysa artırmak sorunu büyütür.
memory_limit ve WP_MEMORY_LIMIT: wp-config.php içinde WP_MEMORY_LIMIT tanımlı değilse tek siteli kurulumda bu sabit 40 MB olur. WordPress, PHP'nin memory_limit değerini yalnızca bu sabitten düşükse yükseltir, hiçbir zaman düşürmez; php.ini'de 128 MB yazıyorsa site 128 MB ile çalışır. WooCommerce'in önerdiği 256 MB için wp-config.php dosyasına define( 'WP_MEMORY_LIMIT', '256M' ); satırını ekleyin ve sonucu WooCommerce > Durum ekranındaki "WordPress bellek sınırı" satırından doğrulayın.
Web sunucusu: HTTP/2 her güncel sunucuda standarttır (Nginx 1.25.1 ve sonrasında http2 on; yönergesiyle açılır). HTTP/3 LiteSpeed'de yerleşiktir; Nginx'te ise varsayılan derlemede gelmeyen ve deneysel olarak işaretlenen ayrı bir modül gerektirir, bu yüzden çoğu kurulumda HTTP/3'ü CDN üzerinden sunmak daha pratiktir. Metin dosyaları için gzip ya da Brotli sıkıştırmayı açın; sürüm parametresi (?ver=) taşıyan CSS, JS ve görsellere uzun süreli Cache-Control başlığı verin. Zaman aşımlarını içten dışa doğru uzayacak şekilde hizalayın: en içte PHP-FPM'in request_terminate_timeout değeri, ardından Nginx'in fastcgi_read_timeout değeri, en dışta CDN'in bekleme süresi gelmelidir. Bu konuda PHP'nin max_execution_time değerine güvenmeyin: php.net'e göre Linux'ta yalnızca betiğin kendi çalışma süresini sayar, veritabanı sorguları ve ağ beklemeleri gibi betik dışında geçen süreyi saymaz; duvar saatine göre işleyen sınır request_terminate_timeout'tur. Nginx'in fastcgi_read_timeout varsayılanı 60 saniyedir ve yukarıdaki PHP-FPM değerinden kısa kalır; bu yüzden 5. bölümdeki örnekte onu 110 saniyeye çektik (request_terminate_timeout 100 saniye, Cloudflare'in varsayılan bekleme süresi 125 saniye). Sıra ters olursa müşteri 504 görürken PHP siparişi arka planda oluşturmaya devam eder ve müşteri yeniden denediğinde çift sipariş oluşabilir.
4. Veritabanı: HPOS, autoload, transient'ler ve oturumlar
HPOS: siparişler için ayrı sipariş tabloları
WooCommerce 8.2'ye kadar siparişler varsayılan olarak ürünler ve sayfalarla birlikte WordPress'in wp_posts ve wp_postmeta tablolarında tutuluyordu. Her sipariş alanı ayrı bir meta satırıydı; sipariş listesini filtrelemek, adrese göre aramak ya da rapor almak büyük meta tablosunda çoklu birleştirme (JOIN) gerektiriyordu. Yüksek performanslı sipariş depolama (HPOS), siparişleri kendi indeksleriyle dört tabloya taşır: wp_wc_orders, wp_wc_order_addresses, wp_wc_order_operational_data ve wp_wc_orders_meta (tablo öneki sizde farklı olabilir). WooCommerce 8.2'den beri yeni kurulumlarda varsayılan olarak açıktır; eski mağazalar kendiliğinden taşınmaz.Durumu WooCommerce > Ayarlar > Gelişmiş > Özellikler (Settings > Advanced > Features) ekranındaki "Sipariş verisi depolama" bölümünden ya da komut satırından görebilirsiniz. Eski bir mağazada geçiş sırası şöyledir:
- Veritabanının tam yedeğini alın ve geri yüklemeyi bir kez deneyin. Geçişi önce hazırlık ortamında yapın.
- Uyumsuz eklentileri bulun: Özellikler ekranındaki "Görüntüleyin ve yönetin" (View and manage) bağlantısı, wp-admin/plugins.php?plugin_status=incompatible_with_feature&feature_id=custom_order_tables adresi ya da wp wc hpos compatibility-info komutu listeyi verir. Uyumsuz bir eklenti etkinken HPOS seçeneği kilitli kalır.
- Woo Subscriptions ya da WooCommerce Bookings gibi kendi gönderi türlerini kullanan eklentileri geçiş boyunca etkin tutun; WooCommerce belgeleri, bunları kapatıp geçiş yapmanın veri tutarsızlığına yol açabileceği konusunda uyarıyor.
- "Uyumluluk modunu etkinleştirin" (Enable compatibility mode) kutusunu işaretleyin. Eşitleme arka planda Action Scheduler üzerinden wc_schedule_pending_batch_processes ve wc_run_batch_process eylemleriyle partiler hâlinde ilerler (11.2.1 kodunda wp_posts'tan HPOS tablolarına parti başına 250 sipariş; woocommerce_orders_cot_and_posts_sync_step_size filtresiyle değişir). Her parti ayrı bir zamanlanmış eylem olduğu için büyük mağazada wp wc hpos sync komutu genellikle çok daha hızlıdır.
- Eşitleme bitince verileri doğrulayın ve "Yüksek performanslı sipariş depolama (önerilir)" seçeneğine geçin.
- Uyumluluk modunu bir süre açık tutun; bu sürede sorun çıkarsa eski depoya anında dönebilirsiniz.
- Her şey yolundaysa uyumluluk modunu kapatın. Açık kaldığı sürece her sipariş yazımı iki depoya birden yapılır; WooCommerce geliştirici blogu da sorunsuz çalışan mağazalara uyumluluk modunu tamamen kapatmayı değerlendirmelerini öneriyor.
Bash:
wp db export hpos-oncesi-$(date +%F).sql # yedek
wp wc hpos status # HPOS açık mı, uyumluluk modu, eşitlenmemiş sipariş sayısı
wp wc hpos compatibility-info # uyumlu, uyumsuz ve beyansız eklentiler
wp wc hpos compatibility-mode enable # tablolar yoksa oluşturur, eşitlemeyi açar
wp wc hpos sync --batch-size=500 # bekleyen siparişleri kopyalar
wp wc hpos verify_data --verbose # iki depo arasındaki farkları raporlar
wp wc hpos enable # ön kontroller geçerse HPOS'u yetkili depo yapar
# ...gözlem dönemi...
wp wc hpos compatibility-mode disable # çift yazmayı durdurur
wp wc hpos enable komutu, eşitlenmemiş sipariş ya da uyumsuz eklenti varsa durur. Uyumluluk modu kapandıktan sonra wp wc hpos cleanup all komutu taşınmış siparişlerin wp_postmeta'daki eski kopyalarını siler. InnoDB, silinen satırların yerini diske kendiliğinden iade etmez; dosyanın küçülmesi için tablonun OPTIMIZE TABLE ile yeniden oluşturulması gerekir, büyük tabloda bunu sakin bir saatte yapın. Temizlikten sonra eski depoya dönmek verilerin yeniden eşitlenmesini gerektirir; bu yüzden temizliği yeni bir yedek aldıktan sonra ve ancak birkaç hafta sorunsuz çalıştıktan sonra yapın.
Özel kod için uyarı: HPOS açıkken siparişleri get_post_meta, update_post_meta, WP_Query ya da doğrudan SQL ile okuyan veya yazan kod yanlış sonuç verir. Siparişlere wc_get_orders(), wc_get_order(), $order->get_meta() ve $order->update_meta_data() gibi WooCommerce CRUD fonksiyonlarıyla erişin. WooCommerce 10.7'den beri uyumluluk modunda "okumada eşitleme" (sync on read) varsayılan olarak kapalıdır: Bir eklenti sipariş verisini doğrudan wp_posts ya da wp_postmeta tablosuna yazarsa bu değişiklik artık HPOS siparişine geri taşınmaz.
wp_options ve autoload
Her istek, otomatik yükleme (autoload) işaretli seçeneklerin tamamını yükler: nesne önbelleği varsa bellekten, yoksa veritabanından. Kaldırılmış eklentilerden kalan ya da büyük dizileri autoload ile saklayan seçenekler, önbelleğe girmeyen her isteğe maliyet ekler. WordPress 6.6 ile autoload sütununa yes ve no değerlerinin yanında on, off, auto, auto-on ve auto-off geldi; sorgularda bunu hesaba katın:
SQL:
-- Toplam autoload boyutu (WordPress 6.6+ değerleri dahil; önek wp_ değilse değiştirin)
SELECT COUNT(*) AS adet, ROUND(SUM(LENGTH(option_value)) / 1024) AS kb
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on', 'auto');
-- En büyük 20 autoload seçeneği
SELECT option_name, autoload, ROUND(LENGTH(option_value) / 1024, 1) AS kb
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on', 'auto')
ORDER BY LENGTH(option_value) DESC
LIMIT 20;
WordPress Site Sağlığı (Site Health) ekranı, autoload toplamı 800 KB'ı aştığında uyarı verir. Büyük satırın hangi eklentiye ait olduğunu adından çıkarın: Eklenti hâlâ kullanılıyorsa ayarlarını ve sürümünü kontrol edin, kaldırılmışsa satırı silin. WordPress çekirdeğine ve WooCommerce'e ait seçeneklerin autoload değerini körlemesine değiştirmeyin.
Transient'ler, oturum tablosu ve tablo büyümesi
Süresi dolmuş geçici veriler (transients) wp_options tablosunda birikebilir. wp transient delete --expired komutu ya da WooCommerce > Durum > Araçlar ekranındaki Expired transients aracı (Türkçe arayüzde "Süresi dolan çerezler" diye çevrilmiş) yalnızca süresi dolanları siler. Tüm transient'leri silmek ise ürün fiyatı ve varyasyon önbelleklerinin baştan hesaplanmasına yol açar; gerekiyorsa sakin bir saatte yapın.Misafir sepetleri wp_woocommerce_sessions tablosunda tutulur. WooCommerce 11.2.1'de misafir oturumları 2 gün, giriş yapmış müşterilerinki 7 gün geçerlidir ve süresi dolanlar 12 saatte bir çalışan woocommerce_cleanup_sessions göreviyle silinir. WooCommerce 10.1'den beri bu temizlik, ödenmemiş siparişlerin iptali ve zamanlanmış indirimler gibi bakım işleri WP-Cron yerine Action Scheduler üzerinden çalışıyor; kuyruk takılırsa tablo büyür.
SQL:
-- Oturum tablosu: toplam ve süresi dolmuş satır
SELECT COUNT(*) AS toplam, SUM(session_expiry < UNIX_TIMESTAMP()) AS suresi_dolmus
FROM wp_woocommerce_sessions;
-- En büyük 15 tablo (table_rows InnoDB'de tahminidir)
SELECT table_name, table_rows,
ROUND((data_length + index_length) / 1024 / 1024, 1) AS mb
FROM information_schema.tables
WHERE table_schema = DATABASE()
ORDER BY (data_length + index_length) DESC
LIMIT 15;
Süresi dolmuş oturum sayısı yüksekse önce Action Scheduler'ı düzeltin (6. bölüm). "Müşteri oturumlarını temizle" aracı tabloyu tamamen boşaltır ve kayıtlı sepetleri de siler; yalnızca bilerek kullanın. wp_postmeta'nın büyümesi genellikle iki kaynaktan gelir: çok varyasyonlu ürünler ve HPOS'a taşınmamış ya da temizlenmemiş eski siparişler. Revizyon, otomatik taslak ve yetim meta gibi genel temizlik için SQL ile WordPress veritabanı temizleme rehberimizi kullanabilirsiniz: Her sorgudan önce yedek alın ve sorguları önce hazırlık ortamında deneyin. Dikkat: Rehberin 8. adımındaki sorgu yalnızca süresi dolanları değil tüm transient'leri siler; WooCommerce'te bu, yukarıda anlattığımız yeniden hesaplama yükünü doğurur. Yalnızca süresi dolanlar için wp transient delete --expired yeterlidir.
InnoDB arabellek havuzu: MariaDB belgelerine göre innodb_buffer_pool_size, sunucunun aktif veri kümesinin büyük kısmını tutabilmelidir; havuz ısındıktan sonra diskten yapılan okumalar (Innodb_buffer_pool_reads), toplam okuma isteklerinin (Innodb_buffer_pool_read_requests) yanında çok küçük kalmalıdır. Yukarıdaki tablo boyutu sorgusunun toplamını (veri ve indeks) başlangıç noktası alın, büyümeye pay bırakın ve oranı izleyin:
SQL:
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
5. Önbellek katmanları ve WooCommerce kuralları
| Katman | Neyi hızlandırır | WooCommerce'te dikkat edilecek nokta |
|---|---|---|
| Tarayıcı ve CDN (statik dosyalar) | Görsel, CSS, JS ve font teslimi | Sürüm parametreli dosyalarda uzun süreli önbellek güvenlidir |
| Tam sayfa önbelleği (Nginx FastCGI cache, LiteSpeed Cache, Varnish, eklenti) | Anonim ziyaretçiye katalog sayfaları | Sepet, ödeme, hesabım ve sepet çerezli istekler asla önbelleğe girmemeli |
| Nesne önbelleği (object cache, Redis) | Önbellek dışı isteklerdeki tekrarlanan seçenek, meta ve oturum okumaları | Kalıcı bir Redis servisi ve bellek sınırı gerekir |
| OPcache | PHP derleme maliyeti | Bellek ya da dosya sayısı yetmezse yeni dosyalar önbelleğe alınamaz |
| InnoDB arabellek havuzu | Disk okumaları | Veri ve indeks büyüdükçe yeniden boyutlandırın |
Tam sayfa önbelleği: neler asla önbelleğe girmemeli
WooCommerce, ayarlarda atanmış sepet, ödeme ve hesabım sayfalarında DONOTCACHEPAGE sabitini tanımlar ve önbelleği engelleyen (no-cache) başlıklar gönderir. Bu sinyale uyan eklentiler bu sayfaları kendiliğinden atlar; ama başlıkları yok sayacak şekilde ayarlanmış sunucu önbellekleri ve "her şeyi önbelleğe al" kuralı olan CDN'ler bu sinyali görmez. Bu yüzden kuralları açıkça yazın:- Yollar: Sepet, ödeme ve hesabım sayfaları ile alt uç noktaları. Türkçe kurulumlarda bu sayfaların kısa adları (slug) genellikle /sepet/, /odeme/ ve /hesabim/ olur; kendi adreslerinizi WooCommerce > Ayarlar > Gelişmiş > Sayfa kurulumu ekranından kontrol edin.
- Çerezler: woocommerce_items_in_cart, woocommerce_cart_hash, wp_woocommerce_session_ ile başlayan oturum çerezi ve giriş yapmış kullanıcılar için wordpress_logged_in_ ile başlayan çerez. Bunlardan biri varsa istek önbelleği atlamalıdır.
- İstekler: POST istekleri, ?add-to-cart=, ?wc-ajax=, /wp-json/ (Store API dahil), wp-admin ve wp-login.php.
Nginx FastCGI cache örneği
Code:
# http {} bloğu
fastcgi_cache_path /var/cache/nginx/woo levels=1:2 keys_zone=WOO:64m max_size=2g inactive=60m use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# server {} bloğu
set $skip_cache 0;
if ($request_method !~ ^(GET|HEAD)$) { set $skip_cache 1; }
if ($args ~* "(^|&)(add-to-cart|wc-ajax|wc-api|rest_route|preview|s)=") { set $skip_cache 1; }
if ($request_uri ~* "^/(cart|checkout|my-account|sepet|odeme|hesabim)(/|\?|$)") { set $skip_cache 1; }
if ($request_uri ~* "^/(wp-admin|wp-json|wp-login\.php|xmlrpc\.php|wp-cron\.php)") { set $skip_cache 1; }
if ($http_cookie ~* "woocommerce_items_in_cart|woocommerce_cart_hash|wp_woocommerce_session_|wordpress_logged_in_|wp-postpass_|comment_author_") { set $skip_cache 1; }
location ~ \.php$ {
include snippets/fastcgi-php.conf; # Debian/Ubuntu; diğer dağıtımlarda fastcgi_params ve SCRIPT_FILENAME
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_read_timeout 110s; # request_terminate_timeout'tan uzun, CDN zaman aşımından kısa
fastcgi_cache WOO;
fastcgi_cache_valid 200 301 10m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_cache_background_update on;
fastcgi_cache_lock on;
add_header X-Cache $upstream_cache_status always;
}
fastcgi_cache_bypass isteğin önbellekten okunmamasını, fastcgi_no_cache yanıtın önbelleğe yazılmamasını sağlar; ikisi birlikte kullanılmalıdır. fastcgi_cache_lock, önbellekte henüz bulunmayan bir sayfa için PHP'ye aynı anda yalnızca tek bir istek gönderir ve aynı sayfayı isteyen diğerlerini bekletir; süresi dolmuş sayfalarda ise use_stale updating ve background_update, yenileme arka planda sürerken ziyaretçilere eski kopyayı sunar. Nginx, Set-Cookie başlığı taşıyan yanıtları varsayılan olarak önbelleğe almaz; fastcgi_ignore_headers listesine Set-Cookie'yi asla eklemeyin, çünkü oturum çerezi oluşturan bir yanıt önbelleğe girerse başka bir müşteriye de gönderilir. Kuralı test etmek için önce sepet sayfasında X-Cache başlığının hiçbir zaman HIT olmadığını, ardından ürün sepete eklendikten sonra kategori sayfasının BYPASS döndüğünü curl ya da tarayıcıyla doğrulayın.
LiteSpeed
LiteSpeed ya da OpenLiteSpeed kullanıyorsanız sunucu kuralı yazmak yerine LiteSpeed Cache eklentisini kullanın. Eklenti WooCommerce'i tanır: Sepet, ödeme ve hesabım sayfaları varsayılan olarak önbellek dışındadır ve yalnızca WooCommerce etkinken görünen bir WooCommerce sekmesi vardır. Belgeler WooCommerce ile ESI'yi (Edge Side Includes) açmayı öneriyor; stok değiştiğinde ürün ve kategori sayfalarının ne kadar agresif temizleneceğini Product Update Interval ayarı belirler. Ek dışlamaları Cache > Excludes altındaki Do Not Cache URIs ve Do Not Cache Cookies alanlarına yazın. Ödeme sayfası WooCommerce ayarlarında doğru sayfaya atanmamışsa dışlama da çalışmaz.Önbelleği temizleme
Stok ya da fiyat değiştiğinde önbellekteki sayfa güncelliğini yitirir. Sepet ve ödeme her zaman güncel stok ve fiyatla hesaplandığı için bu bir veri hatası yaratmaz, ama müşteri tükenmiş bir ürünü stokta görebilir. Açık kaynak Nginx'te önbellek temizleme yönergesi (fastcgi_cache_purge) yoktur, yalnızca ticari sürümde bulunur. Ya yukarıdaki örnekteki gibi kısa bir süre (10 dakika) kullanın ya da üçüncü taraf bir temizleme modülü ve onu tetikleyen bir eklentiyle çalışın. LiteSpeed Cache ve yönetilen hosting önbelleklerinin çoğu, ürün güncellendiğinde ilgili sayfaları kendisi temizler.Redis nesne önbelleği: neyi çözer, neyi çözmez?
WordPress'in nesne önbelleği (object cache) varsayılan olarak yalnızca tek bir istek boyunca yaşar. Kalıcı bir arka uç olarak Redis eklendiğinde seçenekler, meta verileri, terimler, transient'ler ve WooCommerce oturum verileri istekler arasında bellekte kalır; önbelleğe girmeyen sepet, ödeme, hesabım ve yönetim isteklerinde tekrarlanan küçük sorgular azalır. Çözmediği şeyler de net: tek seferlik ağır sorgular (yavaş bir rapor, LIKE ile sipariş araması), dış API beklemeleri ve ağır PHP kodu. Query Monitor'da süre çok sayıda küçük ve tekrarlanan sorguya dağılıyorsa Redis yardımcı olur; süreyi birkaç yavaş sorgu alıyorsa önce onları düzeltin.
PHP:
// wp-config.php ("That's all, stop editing!" satırından önce)
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
define( 'WP_REDIS_DATABASE', 0 ); // aynı Redis'te birden fazla site varsa her birine ayrı numara verin
define( 'WP_REDIS_PREFIX', 'magaza1:' ); // anahtar çakışmalarını önler
Code:
# /etc/redis/redis.conf: sınırsız büyümeyi önleyin
maxmemory 512mb
maxmemory-policy allkeys-lru
Örnekteki sabitler Till Krüss'ün Redis Object Cache eklentisine aittir; eklentiyi kurduktan sonra wp redis enable ile etkinleştirip wp redis status ile bağlantıyı doğrulayın. 64 bit sistemlerde Redis'in varsayılan bellek sınırı yoktur; Redis belgeleri, anahtarların küçük bir kısmının çok daha sık okunduğu önbellek senaryoları için allkeys-lru politikasını iyi bir varsayılan olarak öneriyor. Redis'i yalnızca önbellek olarak kullanıyorsanız diske kalıcılık (persistence) zorunlu değildir. Nesne önbelleği etkinken Özellikler ekranındaki "HPOS Verileri Önbelleğe Alma" (HPOS Data Caching) seçeneği sipariş verilerini de önbelleğe alabilir; varsayılan olarak kapalıdır, önce hazırlık ortamında deneyin.
CDN
CDN'in ilk işi statik dosyalardır. HTML'i de CDN'de önbelleğe alacaksanız yukarıdaki yol, çerez ve istek dışlamalarının aynısını CDN kuralına da yazın; çereze göre atlama gibi koşulların hangi planda desteklendiği sağlayıcıya göre değişir. Sepet ya da ödeme sayfasının CDN'den HIT ile geldiğini görürseniz kuralı hemen kapatın: Bu bir hız sorunu değil, kişisel veri sızıntısıdır.6. Arka plan işleri: WP-Cron ve Action Scheduler
WP-Cron gerçek bir zamanlayıcı değildir; vadesi gelmiş görevleri sayfa isteği geldiğinde tetikler. Tam sayfa önbelleği isteklerin çoğunu PHP'ye uğratmadan karşıladığında görevler gecikir, trafik yüksekken de ziyaretçi isteklerine ek iş biner. WooCommerce'in iş kuyruğu Action Scheduler'dır: Webhook teslimi ve Analytics verilerinin güncellenmesi gibi işleri, 10.1'den beri de oturum temizliği, ödenmemiş sipariş iptali, zamanlanmış indirimler ve log dosyalarının temizliği gibi bakım görevlerini o çalıştırır. Kuyruk WP-Cron üzerinden dakikada bir tetiklenir; ayrıca yönetim paneli açıkken en fazla dakikada bir kez arka planda bir çalıştırma isteği başlatılır. Yani WP-Cron'u kapatıp yerine bir şey koymazsanız kuyruk yalnızca biri yönetim panelini açtığında ilerler.
PHP:
// wp-config.php: sayfa isteklerinin cron tetiklemesini kapatın
define( 'DISABLE_WP_CRON', true );
Bash:
# Site kullanıcısının crontab'ına ekleyin (ör. crontab -u www-data -e); yollar sunucuya göre değişir
* * * * * cd /var/www/magaza && flock -n /tmp/wp-cron-magaza.lock /usr/local/bin/wp cron event run --due-now --quiet
# WP-CLI yoksa (ör. paylaşımlı hosting panelindeki cron ekranı):
# * * * * * wget -q -O /dev/null https://magaza.example.com/wp-cron.php
wp cron event run --due-now, çakışan çalıştırmaları önlemek için WordPress'in doing_cron kilidine uyar; flock buna ek bir güvencedir. Cron'un kullandığı PHP CLI sürümünün PHP-FPM ile aynı olduğunu php -v ile kontrol edin; aksi hâlde görevler başka bir PHP sürümüyle ve başka eklenti uyumluluğuyla çalışır. Ardından kuyruğun sağlığını haftada bir kontrol edin:
Bash:
wp action-scheduler status # durum başına sayı, en eski ve en yeni tarih
wp action-scheduler action list --status=failed --per_page=20 --fields=id,hook,scheduled_date
wp action-scheduler action logs 12345 # başarısız bir eylemin günlüğü
wp action-scheduler run --batch-size=50 --batches=10 # birikmiş kuyruğu elle eritmek için
wp action-scheduler clean --status=complete,canceled --before='14 days ago' --batch-size=500
Action Scheduler varsayılan olarak tamamlanan ve iptal edilen eylemleri 31 gün, başarısız olanları (4.0.0 sürümünden beri) yaklaşık 3 ay saklar. wp_actionscheduler_logs tablosu çok büyümüşse saklama süresini kısaltabilirsiniz:
PHP:
<?php
// wp-content/mu-plugins/as-saklama.php
add_filter( 'action_scheduler_retention_period', function () {
return 14 * DAY_IN_SECONDS; // tamamlanan ve iptal edilen eylemleri 14 gün sakla
} );
Yönetim panelindeki "Eylem Planlayıcı: N süresi geçmiş eylem bulundu; bir şeyler yanlış olabilir." uyarısı, vadesinden en az bir gün geçtiği hâlde bekleyen eylem olduğunda çıkar. Action Scheduler belgeleri birkaç süresi geçmiş eylemi normal sayar; bir günden eski çok sayıda eylem varsa bir sorun olabilir. Olası nedenler şunlardır: WP-Cron kapatılmış ama yerine sistem cron'u kurulmamış; sunucunun kendine yaptığı istekler (loopback) güvenlik duvarı, HTTP temel kimlik doğrulaması ya da bot korumasıyla engelleniyor; bir eylemin geri çağırma fonksiyonu (callback) ölümcül hata veriyor (bu durumda eylem "başarısız" olur ve günlüğünde hata yazar); ya da bir eklenti binlerce eylemi bir anda kuyruğa ekledi. Varsayılan çalıştırıcı eylemleri 25'erli gruplar hâlinde alır, her çalıştırmada yaklaşık 30 saniyelik süre sınırına ya da bellek sınırına yaklaşana kadar devam eder ve aynı anda yalnızca bir grup işler. Action Scheduler belgeleri büyük kuyruklar için WP-CLI'yi öneriyor, çünkü komut satırındaki çalıştırma web isteğinin zaman aşımlarına takılmaz; birikmeleri wp action-scheduler run ile ayrı bir cron'dan eritebilirsiniz.
7. Ön yüz: sepet parçacıkları, ödeme istekleri ve görseller
Sepet parçacıkları (cart fragments): Klasik mini sepet bileşeni, önbellekli sayfalarda sepet içeriğini güncel tutmak için ?wc-ajax=get_refreshed_fragments isteği gönderir. Bu istek önbelleğe alınamaz ve WordPress'i baştan sona yükler. WooCommerce 7.8'den beri betik her sayfada değil, yalnızca klasik Sepet (Cart) bileşeni sayfada gösterildiğinde ya da bir tema veya eklenti onu bağımlılık olarak çağırdığında yüklenir; Storefront gibi bazı temalar bileşeni şablona sabit olarak yerleştirir. Kontrol etmek için önbellekli bir blog sayfası açın ve Network sekmesinde get_refreshed_fragments isteğini arayın. Çözüm sırası şöyledir: Store API kullanan ve cart fragments'a ihtiyaç duymayan Mini Sepet (Mini-Cart) bloğuna geçin; geçemiyorsanız WooCommerce geliştirici blogundaki filtreyle betiğin yalnızca mağaza sayfalarında çalışmasını sağlayın:
PHP:
add_filter( 'woocommerce_get_script_data', function ( $script_data, $handle ) {
if ( 'wc-cart-fragments' === $handle ) {
if ( is_woocommerce() || is_cart() || is_checkout() ) {
return $script_data;
}
return null; // betik yüklenir ama çalışmaz
}
return $script_data;
}, 10, 2 );
Ödeme istekleri: Kasım 2023'te yayımlanan 8.3 sürümünden sonra kurulan mağazalarda sepet ve ödeme varsayılan olarak bloktur ve Store API'yi (/wp-json/wc/store/v1/) kullanır. Klasik kısa kodlu (shortcode) ödeme ise adres ya da kargo seçimi değiştikçe ?wc-ajax=update_order_review isteği gönderir ve her istekte kargo ücretleri yeniden hesaplanır. Canlı fiyat sorgulayan bir kargo eklentisi bu istekleri saniyelerce bekletebilir; eklentinin önbellek ve zaman aşımı ayarlarını kontrol edin.
Görseller: WooCommerce ürün ızgarası için woocommerce_thumbnail, ürün sayfası için woocommerce_single (varsayılan genişlik 600 piksel) ve galeri küçük resimleri için woocommerce_gallery_thumbnail boyutlarını kullanır. Klasik temalarda bu boyutları Görünüm > Özelleştir > WooCommerce > Ürün görselleri ekranından ayarlayın; değişikliği kaydettiğinizde WooCommerce küçük resimleri arka planda kendisi yeniden oluşturur (çoklu site kurulumlarında bu otomatik çalışmaz, orada wp media regenerate komutunu kullanabilirsiniz). Ürün sayfasının ana görseli çoğu zaman Largest Contentful Paint (LCP) öğesidir: Onu tembel yüklemeye (lazy-load) bırakmayın, doğru boyutta ve WebP ya da AVIF gibi modern bir biçimde sunun.
Betikler: Sayfa oluşturucular, pazarlama pikselleri ve canlı sohbet araçları çoğu zaman her sayfaya betik ekler. Query Monitor'ın Scripts panelinden hangi sayfada neyin yüklendiğini görün ve gerekmeyen yerden kaldırın. Order Attribution özelliği de her sayfaya küçük bir izleme betiği (sourcebuster.js) ekler; sipariş kaynağı verisini kullanıyorsanız tutun, kullanmıyorsanız Özellikler ekranından kapatabilirsiniz.
Core Web Vitals: web.dev'in "iyi" eşikleri, sayfa yüklemelerinin 75. yüzdeliğinde LCP için 2,5 saniye, INP için 200 milisaniye ve CLS için 0,1'dir. Ürün sayfalarında INP'yi bozan yaygın nedenler ağır varyasyon seçicileri ve galeri betikleri; CLS'yi bozanlar ise sonradan yüklenen yorum, rozet ve taksit bileşenleridir.
8. Eklenti ve tema denetimi
- Gerçek verinin bir kopyasıyla, hazırlık ortamında çalışın.
- Yavaş sayfaları (ürün, sepet, ödeme, sipariş listesi) Query Monitor açıkken ziyaret edin. Queries by Component görünümünde eklentileri sorgu sayısına ve süresine göre sıralayın; HTTP API Calls panelinde dış istekleri ve sürelerini not edin.
- Şüpheli eklentiyi devre dışı bırakıp aynı ölçümü tekrarlayın. Neden belirsizse eklentileri yarı yarıya kapatarak ilerleyin (ikiye bölme).
- Komut satırını tercih ediyorsanız wp-cli/profile-command paketindeki wp profile stage ve wp profile hook --all komutları aynı soruyu sunucuda yanıtlar.
- Örtüşen eklentileri ayıklayın: iki ayrı tam sayfa önbelleği, birden fazla güvenlik tarayıcısı, aynı işi yapan iki SEO ya da filtre eklentisi.
- Ürün şablonunda ağır bir sayfa oluşturucu kullanılıyorsa önbelleksiz ürün sayfası süresini sayfa oluşturucusuz bir şablonla karşılaştırın.
- Her tuş vuruşunda admin-ajax.php çağıran canlı arama eklentilerini ve sepete eklemede dış servise giden işlemleri ayrıca ölçün.
9. Ölçek ve güvenlik: hazırlık ortamı, arama, botlar ve kart testi
Hazırlık ortamı ve yedek: WooCommerce, PHP ve büyük eklenti güncellemelerini önce hazırlık ortamında deneyin; HPOS geçişi, toplu ürün aktarımı ya da tablo temizliği gibi her veri işleminden önce yedek alın ve geri yüklemeyi test edin. WooCommerce'in değişiklik günlüğünde (changelog) her sürümde "Performance" etiketli çok sayıda düzeltme yer alıyor; örneğin 11.1.0, çok ürünlü sepetlerde sepet sayfasının SQL sorgu sayısını azalttı. Bu yüzden güncel kalmak da bir hızlandırma adımıdır.Büyük katalogda arama: WooCommerce'in varsayılan ürün araması veritabanında metin eşleştirmesiyle çalışır ve büyük kataloglarda yavaşlayabilir. ElasticPress gibi bir eklentiyle aramayı ve filtrelemeyi Elasticsearch'e taşımak hem hızı hem sonuç kalitesini artırır; OpenSearch kullanmayı düşünüyorsanız eklentinin uyumluluk belgesini kontrol edin. Yönetimde sipariş araması yavaşsa yalnızca HPOS ile çalışan ve 9.0'dan beri deneysel olarak sunulan "HPOS Tam metin arama dizinleri" (HPOS Full text search indexes) seçeneğini değerlendirin.
Botlar ve kart testi saldırıları: Kart testi (card testing), çalınmış kart numaralarının küçük tutarlı siparişlerle denenmesidir. Belirtileri: kısa sürede çok sayıda başarısız ya da ödeme bekleyen misafir siparişi, ödeme uç noktalarına aynı IP bloklarından yoğun POST trafiği ve ödeme sağlayıcısından gelen ret bildirimleri. Bu saldırı işlemci, veritabanı ve e-posta yükünün yanında sağlayıcı ücretleri ve hesap riski de getirir. Önlemleri katman katman alın:
- Özellikler ekranındaki Rate limit Checkout seçeneği (Türkçe arayüzde "Ücret sınırı Ödeme" adıyla görünüyor), blok ödemede ve Store API /checkout uç noktasında istekleri IP ya da kullanıcı başına 60 saniyede 3 ile sınırlar. Klasik ödeme (?wc-ajax=checkout) bu kapsamda değildir. Store API'nin vekil sunucu desteği (proxy_support) varsayılan olarak kapalı olduğundan, site bir CDN arkasındaysa PHP'nin gerçek ziyaretçi IP'sini gördüğünden emin olun (aşağıdaki real_ip ayarı); aksi hâlde bu sınır, CDN'in az sayıdaki IP adresi üzerinden gelen tüm müşterileri birlikte sayar.
- Store API'nin genel hız sınırlaması varsayılan olarak kapalıdır; woocommerce_store_api_rate_limit_options filtresiyle açılır (varsayılan: 10 saniyede 25 istek).
- Web sunucusunda hassas uç noktalara hız sınırı koyun (örnek aşağıda); WAF ya da CDN bot kurallarını etkinleştirin.
- Ödeme sayfasına CAPTCHA (ör. Cloudflare Turnstile ya da reCAPTCHA) eklemek etkilidir ama gerçek müşteriye de sürtünme ekler; saldırı dönemlerinde açmak dengeli bir yaklaşımdır.
- Ödeme sağlayıcınızın dolandırıcılık araçlarını ve 3D Secure kurallarını devreye alın.
Code:
# http {} bloğu: yalnızca hassas istekler için anahtar üret (boş anahtar sayılmaz)
map $request_uri $woo_hassas {
default "";
"~wc-ajax=(checkout|add_to_cart|apply_coupon)" $binary_remote_addr;
"~^/wp-json/wc/store(/v[0-9]+)?/checkout" $binary_remote_addr;
}
limit_req_zone $woo_hassas zone=woo_hassas:10m rate=20r/m;
# server {} bloğu
limit_req zone=woo_hassas burst=10 nodelay;
limit_req_status 429;
Bu değerler bir başlangıç noktasıdır; erişim günlüğünde 429 yanıtlarını izleyerek ayarlayın. Mobil operatörler ve kurumsal ağlar çok sayıda kullanıcıyı tek bir IP adresinin arkasında toplayabildiği için (operatör düzeyinde NAT, CGNAT) sınırları fazla sıkı tutmayın. Site bir CDN ya da ters vekil sunucunun (reverse proxy) arkasındaysa gerçek ziyaretçi IP'sini real_ip modülüyle (set_real_ip_from ve real_ip_header) alın; aksi hâlde sınır CDN'in IP'sine uygulanır ve herkes birlikte engellenir.
E-postalar: Sipariş e-postaları varsayılan olarak ödeme isteğinin içinde gönderilir; yavaş bir SMTP sunucusu sipariş onayını saniyelerce bekletebilir. WooCommerce 10.8 ve sonrasında Özellikler ekranındaki Deferred emails ("Ertelenmiş e-postalar") seçeneği, varsayılan olarak kapalıdır ve açıldığında bu e-postaları Action Scheduler üzerinden istek dışında gönderir. Açmadan önce Action Scheduler'ın düzgün çalıştığından emin olun.
10. Sorun giderme tablosu
| Belirti | Olası neden | Kontrol ve çözüm |
|---|---|---|
| Yönetimde sipariş listesi ve arama çok yavaş | Siparişler hâlâ wp_posts ve wp_postmeta'da; uyumluluk modu açık; eklentilerin meta sorguları | wp wc hpos status; HPOS'a geçin ve gözlemden sonra uyumluluk modunu kapatın; sorguyu üreten eklentiyi Query Monitor'da bulun |
| Ödeme dönüp duruyor ya da zaman aşımına uğruyor | Yavaş kargo, vergi ya da ödeme API'si; e-posta gönderimi; dolu PHP-FPM kuyruğu; stok satırlarında kilit beklemesi | Query Monitor HTTP API paneli, PHP-FPM slowlog ve durum sayfası, yavaş sorgu günlüğü; Deferred emails; dış API zaman aşımlarını kısaltın |
| "N süresi geçmiş eylem bulundu" uyarısı | WP-Cron çalışmıyor; loopback isteği engelli; bir eylem ölümcül hata veriyor; ani iş yığılması | Sistem cron'unu kurun; wp action-scheduler status; başarısız eylemlerin günlükleri; birikmeyi wp action-scheduler run ile eritin |
| Botlar yüzünden yüksek CPU | Filtre ve sıralama parametreli URL kombinasyonlarının taranması; ?add-to-cart= bağlantıları; wc-ajax istek yağmuru | Erişim günlüğünde en çok istenen URL'leri ve IP'leri çıkarın; robots.txt ile filtre ve sepete ekleme parametrelerini kapatın (yalnızca kurallara uyan botlar dikkate alır); diğerleri için hız sınırı ve WAF |
| 502 Bad Gateway ya da 504 Gateway Timeout | pm.max_children doldu; PHP süreci bellek yetersizliğinden öldürüldü; zaman aşımları uyumsuz | Durum sayfasında max children reached; dmesg ya da journalctl'de OOM kayıtları; zaman aşımlarını hizalayın. Cloudflare arkasında aynı durum, varsayılan 125 saniyelik bekleme aşılınca 524 olarak görünür |
| Önbellek başka müşterinin sepetini ya da yanlış mini sepeti gösteriyor | Sepet çerezleri ya da yolları dışlanmamış; Set-Cookie yok sayılıyor; CDN HTML'i önbelleğe alıyor | curl -sI ile sepet sayfasındaki önbellek başlığına bakın (HIT olmamalı); kuralları düzeltip tüm önbelleği temizleyin |
| Her sayfada ?wc-ajax=get_refreshed_fragments isteği | Klasik mini sepet bileşeni; tema ya da eklenti bağımlılığı | Mini Sepet bloğuna geçin ya da 7. bölümdeki filtreyi kullanın |
Sık sorulan sorular
Eski bir mağazada HPOS'u açmak güvenli mi?Eklentileriniz uyumlu olduğunu beyan ediyorsa, yedek alıp önce hazırlık ortamında denediyseniz ve geçişi uyumluluk modu açıkken yaptıysanız evet. Uyumluluk modu açık kaldığı sürece eski depoya anında dönebilirsiniz. Asıl risk, siparişlere WordPress meta fonksiyonlarıyla ya da doğrudan SQL ile erişen özel koddur; geçişten önce bu kodu WooCommerce CRUD fonksiyonlarına taşıyın.
Redis şart mı?
Hayır. Trafiğin büyük kısmı anonimse ve tam sayfa önbelleğinden karşılanıyorsa küçük bir mağaza Redis olmadan da iyi çalışır. Giriş yapmış müşteriler, yoğun sepet ve ödeme trafiği ya da kalabalık bir yönetim ekibi varsa ve Query Monitor süreyi çok sayıda tekrarlanan küçük sorguda gösteriyorsa Redis belirgin fark yaratır.
Hangi önbellek eklentisini seçmeliyim?
Sunucunuza uygun olanı: LiteSpeed'de LiteSpeed Cache, Nginx'te sunucu düzeyinde FastCGI cache (temizleme için yardımcı bir eklentiyle) ya da hosting firmanızın yönetilen önbelleği. Ölçüt marka değil; WooCommerce dışlamalarını doğru yapması ve stok ya da fiyat değiştiğinde ilgili sayfaları temizlemesidir. Aynı anda iki tam sayfa önbelleği çalıştırmayın.
Ana sayfa hızlı ama ödeme neden yavaş?
Ana sayfa önbellekten gelir. Ödeme ise her seferinde PHP'de çalışır, kargo ve vergiyi hesaplar, çoğu zaman dış API'lere gider, siparişi ve stok rezervasyonunu veritabanına yazar ve e-posta gönderir. Önbellek bu yolu hızlandıramaz; süreyi hangi adımın aldığını Query Monitor ve PHP-FPM slowlog ile bulun.
Kaç PHP worker gerekir?
Saniyedeki önbellek dışı istek sayısını ortalama yanıt süresiyle çarpın ve dalgalanma için pay ekleyin; sonra sonucu bellek bütçesiyle (worker başına ölçtüğünüz RSS) ve CPU gözlemiyle sınırlayın. Son kararı PHP-FPM durum sayfası verir: Kuyruk boşsa ve max children reached artmıyorsa sayı yeterlidir.
Sonuç: bir haftalık eylem listesi
WooCommerce'i hızlandırmanın sırası bellidir: önce ölçüm, sonra arka plan işleri ve önbellek kuralları gibi ucuz ve düşük riskli adımlar, en son da HPOS geçişi ve sunucu büyütme gibi planlama isteyen işler.- 1. gün: 1. bölümdeki ölçüm tablosunu doldurun; Query Monitor'ı, yavaş sorgu günlüğünü ve PHP-FPM durum sayfasını açın.
- 2. gün: DISABLE_WP_CRON ve sistem cron'unu kurun; wp action-scheduler status ile başarısız ve süresi geçmiş eylemleri inceleyin.
- 3. gün: Tam sayfa önbelleği kurallarını sepet, ödeme, hesabım ve sepet çerezleri için doğrulayın; curl ile HIT ve BYPASS testini yapın.
- 4. gün: PHP sürümünü, OPcache ve PHP-FPM ayarlarını gözden geçirin; gerekiyorsa Redis nesne önbelleğini ekleyin.
- 5. gün: Autoload boyutunu, süresi dolmuş transient'leri, oturum tablosunu ve en büyük tabloları kontrol edin.
- 6. gün: HPOS durumunu ve eklenti uyumluluğunu çıkarın; geçişi hazırlık ortamında planlayın.
- 7. gün: Sepet parçacıkları, görseller ve eklenti denetimini yapın; ölçüm tablosunu yeniden doldurup ilk günle karşılaştırın.
When someone says their WooCommerce store is slow, there is rarely a single problem. Home and category pages reach anonymous visitors from a full-page cache and arrive quickly; the cart, checkout and My Account pages, add-to-cart requests and the admin are rebuilt by PHP and the database on every request. That uncached path is what actually slows a store down, and it is where a speed-up plan has to start. This guide measures first, then works through hosting size, PHP-FPM, the database and High-Performance Order Storage (HPOS), caching rules, background jobs in Action Scheduler and the front end, in order of priority. We checked setting names and commands against WooCommerce 11.2.1 and the Action Scheduler 4.1.0 it bundles; older versions may lack some options. Generic WordPress tuning comes up only where it directly affects WooCommerce. If you are still deciding on a platform, start with our comparison of Shopify, WooCommerce, Magento and custom builds.
1. Measure first: which request is actually slow?
Before you change any setting, take the measurements below and write them into a table. Repeat the same measurements after each change, with the same product and at similar times of day; otherwise you cannot tell which step helped.| Measurement | How | What to look for |
|---|---|---|
| Cached page (category, anonymous visitor) | curl, browser developer tools | A cache HIT in the response headers and time to first byte (TTFB) |
| Uncached product page | curl with a unique query string | The raw cost of PHP and the database. web.dev suggests 0.8 seconds or less as a rough TTFB target |
| Add to cart | DevTools > Network: ?wc-ajax=add_to_cart or a Store API request | Duration of the request and any extra AJAX calls that follow it |
| Checkout page and Place order | Network: ?wc-ajax=update_order_review and ?wc-ajax=checkout (classic) or /wp-json/wc/store/v1/checkout (block) | Time spent waiting on shipping, tax and payment provider APIs shows up here |
| Admin: order list and order edit screen | Query Monitor | Query count, the slowest query and which plugin issued it |
| Background jobs | WooCommerce > Status > Scheduled Actions | Counts of pending, past-due and failed actions |
Bash:
# Cached page: TTFB and total time
curl -s -o /dev/null -w "TTFB %{time_starttransfer}s total %{time_total}s\n" https://shop.example.com/product-category/shoes/
# Was it a cache hit? (header names differ by server and CDN)
curl -sI https://shop.example.com/product-category/shoes/ | grep -iE "x-cache|x-litespeed-cache|cf-cache-status|^age|cache-control"
# Uncached measurement: a unique query string forces a MISS in most caches
curl -s -o /dev/null -w "TTFB %{time_starttransfer}s\n" "https://shop.example.com/product/sample-product/?probe=$(date +%s)"
The tools you will use:
- Query Monitor: groups every page's database queries by plugin and theme, flags slow, duplicate and failing queries, and lists hook callbacks and outgoing HTTP requests with their timings. It is the fastest way to see how many seconds a shipping or payment API holds up checkout. Deactivate it when you have finished measuring.
- Slow query log: catches heavy queries that never show up on a single page but pile up over a day. Summarize it with mysqldumpslow (mariadb-dumpslow on MariaDB) or pt-query-digest.
- PHP-FPM status page: shows idle and busy processes, the listen queue and the "max children reached" counter; setup is in section 3.
- Scheduled Actions screen: shows the health of the Action Scheduler queue; details in section 6.
- APM (optional): an application performance monitoring tool such as New Relic, Datadog or Tideways traces slow transactions down to the function and keeps the history, so you can look back at what happened during a sale.
INI:
# /etc/mysql/conf.d/slow.cnf (path varies by distribution); restart the service afterwards
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
long_query_time defaults to 10 seconds; 1 second is a more useful starting point for a store. Lower the threshold once you have a few days of data.
2. Hosting size: cores, RAM and the number of PHP workers
As long as anonymous catalog traffic is served from cache, server load is decided by the requests that bypass it: cart, checkout, My Account, ?wc-ajax= calls, the Store API (/wp-json/wc/store/...), logged-in customers and the admin. Each of those occupies one PHP-FPM worker while it runs. Little's law gives a rough estimate of how many workers you need at once:Concurrent requests ≈ uncached requests per second × average response time (seconds)
Example: during a sale the store receives 6 uncached requests per second and they take 0.5 seconds on average, so 3 workers are busy on average. Requests do not arrive evenly, so leaving 3-4 times headroom and aiming for 9-12 workers is a sensible starting point. These figures illustrate the method; they are not measurements. Take the request rate from your access log and the response time from the measurements above.
CPU: a PHP request runs on a single thread, so it uses at most one core while it is actually executing. If requests spend most of their time waiting on the database or an external API, more workers than cores is useful; if they are CPU-bound, extra workers only form a queue inside the server and slow every request down. After raising the worker count, watch CPU usage and response times together: if CPU is pinned and response times grow, the answer is more cores or lighter code, not more workers.
RAM: build the budget in this order: operating system and web server, InnoDB buffer pool, Redis, and whatever remains for PHP-FPM. PHP's memory_limit is a per-request ceiling, not what each worker actually uses, so size the pool from the measured resident memory (RSS) of your workers:
Bash:
# Average RSS of running php-fpm processes (includes the master process; fine for a rough estimate)
ps -eo rss,comm | awk '/php-fpm/ {s+=$1; n++} END {printf "%d processes, average %.0f MB\n", n, s/n/1024}'
The table below is a worked example for a single server running Nginx, PHP-FPM, MariaDB and Redis; the values are assumptions, not measurements:
| Item | 4 vCPU, 8 GB RAM | 8 vCPU, 16 GB RAM |
|---|---|---|
| OS, Nginx, monitoring agent | 1 GB | 1.5 GB |
| InnoDB buffer pool (based on data and index size) | 2 GB | 5 GB |
| Redis (maxmemory) | 512 MB | 1 GB |
| Left for PHP-FPM | about 4.5 GB | about 8.5 GB |
| Memory ceiling at an assumed 120 MB per worker | about 38 workers at most | about 72 workers at most |
That figure is only a memory ceiling: 38 workers doing CPU-bound work at the same time on 4 cores will choke them. Set pm.max_children to the lower of the memory ceiling and what your CPU observations allow, then fine-tune it by watching the queue on the PHP-FPM status page.
Signs that you have outgrown shared hosting:
- 508 errors on CloudLinux-based shared hosting, meaning the entry processes (concurrent connections) limit is full. Hitting the memory or process limit produces 500 or 503 errors instead, while CPU or I/O limits just make the site slower.
- Checkout timing out during sales while working fine at quiet hours.
- No access to a real system cron, SSH and WP-CLI, Redis or PHP-FPM settings, which rules out most of the steps in this guide.
- Past-due actions piling up in Scheduled Actions and a sessions table that keeps growing.
3. PHP version, OPcache and PHP-FPM
WooCommerce's server recommendations page (for 10.8 and later) recommends PHP 8.3 or newer and, at the time of writing, says it is tested up to PHP 8.4; it also lists MySQL 8.0+ or MariaDB 10.6+, HTTPS and a WordPress memory limit of at least 256 MB. It notes that WooCommerce still runs on legacy versions such as PHP 7.4 and MySQL 5.6, which have reached end of life. According to php.net, security support ends on 31 December 2026 for PHP 8.2 and on 31 December 2027 for PHP 8.3. Test a PHP upgrade on staging first, paying particular attention to payment and shipping plugins.OPcache keeps compiled PHP files in memory. When it is off, every file is recompiled on every request; when it is full, the files that no longer fit are recompiled every time. The defaults (128 MB of memory, an 8 MB interned strings buffer, 10000 files) can be tight for a store: the WooCommerce 11.2.1 release package alone ships more than 3,600 PHP files, and WordPress core, the theme and other plugins add many more.
INI:
; /etc/php/8.3/fpm/conf.d/10-opcache.ini (Debian/Ubuntu path; varies by distribution)
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
; Keep timestamp checks on if plugins are updated from the admin
opcache.validate_timestamps=1
opcache.revalidate_freq=60
opcache.validate_timestamps=0 removes file checks altogether, but then plugin updates made from the dashboard do not take effect until PHP-FPM reloads. Use it only if you deploy with a script that reloads PHP-FPM afterwards. revalidate_freq=60 means a changed file is noticed within a minute at most. Do not prioritize JIT: the PHP 8.0 release notes say it delivers large gains on synthetic benchmarks while typical application performance stays on par with PHP 7.4.
PHP-FPM process manager: pm = static starts a fixed number of workers up front, which makes memory use predictable and avoids spawn latency under sudden load; on a server dedicated to the store it is the simplest choice. pm = dynamic keeps the number of idle workers between a minimum and maximum spare count and suits servers that also run other workloads. pm = ondemand starts workers when requests arrive and stops them when idle; it saves memory on low-traffic sites but makes the first requests of a spike wait.
INI:
; /etc/php/8.3/fpm/pool.d/www.conf (Debian/Ubuntu path)
pm = static
pm.max_children = 16 ; from the memory budget and CPU observations
pm.max_requests = 500 ; recycle each worker after 500 requests to contain slow leaks
; If the server runs other workloads, use dynamic instead:
; pm = dynamic
; pm.start_servers = 4
; pm.min_spare_servers = 2
; pm.max_spare_servers = 6
pm.status_path = /fpm-status
request_slowlog_timeout = 5s
slowlog = /var/log/php8.3-fpm-slow.log
request_terminate_timeout = 100s
request_slowlog_timeout writes a PHP backtrace of any request that runs longer than 5 seconds to the slowlog file, which is usually where you find the plugin function a checkout is stuck in. The status page exposes request URLs and resource details, so make it reachable from the server itself only. Add the block below to a server block that answers locally (for example one listening on 127.0.0.1):
Code:
# Nginx: PHP-FPM status page reachable from localhost only (socket path varies by distribution)
location = /fpm-status {
allow 127.0.0.1;
deny all;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
# Read it with: curl -s "http://127.0.0.1/fpm-status?full"
If the listen queue stays above zero and the max children reached counter keeps climbing, requests are waiting for a free worker. Raise pm.max_children if the CPU has headroom; if the CPU is already saturated, raising it makes things worse.
memory_limit and WP_MEMORY_LIMIT: when WP_MEMORY_LIMIT is not defined in wp-config.php, it defaults to 40 MB on a single site. WordPress raises PHP's memory_limit only when it is below that constant and never lowers it, so if php.ini says 128 MB, the site runs with 128 MB. For the 256 MB WooCommerce recommends, add define( 'WP_MEMORY_LIMIT', '256M' ); to wp-config.php and confirm the result in the "WordPress memory limit" row of WooCommerce > Status.
Web server: HTTP/2 is standard on every current server (on Nginx 1.25.1 and later it is switched on with the http2 on; directive). HTTP/3 is built into LiteSpeed; on Nginx it needs a separate module that is not part of the default build and is marked experimental, so serving HTTP/3 from the CDN is often the more practical route. Enable gzip or Brotli for text assets and send long-lived Cache-Control headers for CSS, JS and images that carry a version parameter (?ver=). Align timeouts so that they grow from the inside out: PHP-FPM's request_terminate_timeout innermost, then Nginx's fastcgi_read_timeout, and the CDN's timeout outermost. Do not rely on max_execution_time for this: according to php.net, on Linux it counts only the script's own execution time, not time spent outside it such as database queries and network waits; the wall-clock limit is request_terminate_timeout. Nginx's fastcgi_read_timeout defaults to 60 seconds, shorter than the PHP-FPM value above, which is why the example in section 5 raises it to 110 seconds (with request_terminate_timeout at 100 seconds and Cloudflare's default timeout at 125 seconds). If the order is reversed, the customer sees a 504 while PHP carries on creating the order in the background, and a retry can produce a duplicate order.
4. Database: HPOS, autoload, transients and sessions
HPOS: dedicated order tables
Until WooCommerce 8.2, orders were stored by default in WordPress's wp_posts and wp_postmeta tables alongside products, pages and everything else. Every order field was a separate meta row, so filtering the order list, searching by address or running reports meant multiple joins across a huge meta table. High-Performance Order Storage moves orders into four tables with their own indexes: wp_wc_orders, wp_wc_order_addresses, wp_wc_order_operational_data and wp_wc_orders_meta (your table prefix may differ). It has been on by default for new installations since WooCommerce 8.2; existing stores are not migrated automatically.You can see the current state under WooCommerce > Settings > Advanced > Features, in the Order data storage section, or from the command line. The migration order for an existing store is:
- Take a full database backup and test a restore once. Run the migration on staging first.
- Find incompatible plugins: the "View and manage" link on the Features screen, the URL wp-admin/plugins.php?plugin_status=incompatible_with_feature&feature_id=custom_order_tables or the wp wc hpos compatibility-info command will list them. While an incompatible plugin is active, the HPOS option stays locked.
- Keep plugins that use their own post types, such as Woo Subscriptions or WooCommerce Bookings, active throughout the migration; the WooCommerce documentation warns that deactivating them first can lead to data discrepancies.
- Tick "Enable compatibility mode". Synchronization then runs in the background through Action Scheduler, via the wc_schedule_pending_batch_processes and wc_run_batch_process actions, in batches (250 orders per batch from wp_posts to the HPOS tables in the 11.2.1 code, adjustable with the woocommerce_orders_cot_and_posts_sync_step_size filter). Because each batch is a separate scheduled action, the wp wc hpos sync command is usually much faster on a large store.
- When the sync has finished, verify the data and switch to "High-performance order storage (recommended)".
- Leave compatibility mode on for a while; if anything goes wrong during that period you can switch back to posts storage instantly.
- Once everything is stable, turn compatibility mode off. As long as it is on, every order write goes to both stores; the WooCommerce developer blog also suggests that stores running without issues consider switching compatibility mode off entirely.
Bash:
wp db export before-hpos-$(date +%F).sql # backup
wp wc hpos status # HPOS on?, compatibility mode, unsynced order count
wp wc hpos compatibility-info # compatible, incompatible and undeclared plugins
wp wc hpos compatibility-mode enable # creates the tables if missing and turns sync on
wp wc hpos sync --batch-size=500 # copies pending orders
wp wc hpos verify_data --verbose # reports differences between the two stores
wp wc hpos enable # makes HPOS authoritative if pre-checks pass
# ...observation period...
wp wc hpos compatibility-mode disable # stops the double writes
wp wc hpos enable stops if there are unsynced orders or incompatible plugins. Once compatibility mode is off, wp wc hpos cleanup all deletes the legacy copies of migrated orders from wp_postmeta. InnoDB does not hand the freed space back to the disk by itself; the file only shrinks when the table is rebuilt with OPTIMIZE TABLE, which on a large table belongs in a quiet hour. After the cleanup, going back to posts storage requires a full re-sync, so run the cleanup only after a fresh backup and a few weeks of trouble-free operation.
A warning about custom code: with HPOS enabled, code that reads or writes orders through get_post_meta, update_post_meta, WP_Query or direct SQL returns wrong results. Access orders through WooCommerce CRUD functions such as wc_get_orders(), wc_get_order(), $order->get_meta() and $order->update_meta_data(). Since WooCommerce 10.7, sync on read is disabled by default in compatibility mode: if a plugin writes order data straight into wp_posts or wp_postmeta, that change is no longer pulled back into the HPOS order.
wp_options and autoload
Every request loads all options flagged for autoload, from memory when an object cache is present and from the database otherwise. Leftovers from removed plugins, or plugins that autoload large arrays, add a cost to every uncached request. WordPress 6.6 added on, off, auto, auto-on and auto-off to the autoload column next to yes and no, so account for them in your queries:
SQL:
-- Total autoload size (includes WordPress 6.6+ values; change the wp_ prefix if yours differs)
SELECT COUNT(*) AS options, ROUND(SUM(LENGTH(option_value)) / 1024) AS kb
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on', 'auto');
-- The 20 largest autoloaded options
SELECT option_name, autoload, ROUND(LENGTH(option_value) / 1024, 1) AS kb
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on', 'auto')
ORDER BY LENGTH(option_value) DESC
LIMIT 20;
WordPress Site Health warns when the autoload total exceeds 800 KB. Work out from the option name which plugin owns a large row: if the plugin is still in use, check its settings and version; if it was removed, delete the row. Do not blindly change the autoload flag on options that belong to WordPress core or WooCommerce.
Transients, the sessions table and table growth
Expired transients can accumulate in wp_options. The wp transient delete --expired command, or the Expired transients tool under WooCommerce > Status > Tools, removes only the expired ones. Deleting every transient forces product price and variation caches to be rebuilt, so if you must do it, pick a quiet hour.Guest carts live in the wp_woocommerce_sessions table. In WooCommerce 11.2.1 guest sessions last 2 days and logged-in customer sessions 7 days, and expired rows are removed by the woocommerce_cleanup_sessions job every 12 hours. Since WooCommerce 10.1 this cleanup, along with maintenance jobs such as canceling unpaid orders and starting or ending scheduled sales, runs through Action Scheduler instead of WP-Cron; if the queue stalls, the table grows.
SQL:
-- Sessions table: total and expired rows
SELECT COUNT(*) AS total, SUM(session_expiry < UNIX_TIMESTAMP()) AS expired
FROM wp_woocommerce_sessions;
-- The 15 largest tables (table_rows is an estimate on InnoDB)
SELECT table_name, table_rows,
ROUND((data_length + index_length) / 1024 / 1024, 1) AS mb
FROM information_schema.tables
WHERE table_schema = DATABASE()
ORDER BY (data_length + index_length) DESC
LIMIT 15;
If the expired count is high, fix Action Scheduler first (section 6). The "Clear customer sessions" tool empties the table completely and also deletes saved carts, so use it deliberately. Growth in wp_postmeta usually has two sources: products with many variations, and old orders that were never migrated to HPOS or cleaned up after migration. For general cleanup such as revisions, auto-drafts and orphaned meta, use our guide to cleaning up a WordPress database with SQL: back up before every query and try the queries on staging first. Note that the query in step 8 of that guide deletes all transients, not only expired ones; on WooCommerce that triggers the rebuild load described above, and wp transient delete --expired is enough for the expired ones.
InnoDB buffer pool: the MariaDB documentation says innodb_buffer_pool_size should hold most of the server's active data set; once the pool has warmed up under normal load, reads from disk (Innodb_buffer_pool_reads) should be very few compared with read requests (Innodb_buffer_pool_read_requests). Start from the data plus index total returned by the table size query above, leave room for growth and keep an eye on that ratio:
SQL:
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
5. Cache layers and WooCommerce rules
| Layer | What it speeds up | WooCommerce caveat |
|---|---|---|
| Browser and CDN (static files) | Delivery of images, CSS, JS and fonts | Long cache lifetimes are safe for versioned files |
| Full-page cache (Nginx FastCGI cache, LiteSpeed Cache, Varnish, a plugin) | Catalog pages for anonymous visitors | Cart, checkout, My Account and requests carrying cart cookies must never be cached |
| Object cache (Redis) | Repeated option, meta and session lookups on uncached requests | Needs a persistent Redis service and a memory limit |
| OPcache | PHP compilation cost | When memory or file slots run out, new files stop being cached |
| InnoDB buffer pool | Disk reads | Resize as data and indexes grow |
Full-page cache: what must never be cached
On the cart, checkout and My Account pages assigned in its settings, WooCommerce defines the DONOTCACHEPAGE constant and sends no-cache headers. Caching plugins that honor that signal skip those pages automatically, but server caches configured to ignore headers and CDNs with a "cache everything" rule never see it. Write the rules explicitly:- Paths: the cart, checkout and My Account pages and their endpoints. Check your own slugs under WooCommerce > Settings > Advanced > Page setup; translated installs use localized slugs (a Turkish install, for example, typically has /sepet/, /odeme/ and /hesabim/).
- Cookies: woocommerce_items_in_cart, woocommerce_cart_hash, the session cookie starting with wp_woocommerce_session_, and for logged-in users the cookie starting with wordpress_logged_in_. If any of them is present, the request must bypass the cache.
- Requests: POST requests, ?add-to-cart=, ?wc-ajax=, /wp-json/ (including the Store API), wp-admin and wp-login.php.
Nginx FastCGI cache example
Code:
# http {} block
fastcgi_cache_path /var/cache/nginx/woo levels=1:2 keys_zone=WOO:64m max_size=2g inactive=60m use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# server {} block
set $skip_cache 0;
if ($request_method !~ ^(GET|HEAD)$) { set $skip_cache 1; }
if ($args ~* "(^|&)(add-to-cart|wc-ajax|wc-api|rest_route|preview|s)=") { set $skip_cache 1; }
if ($request_uri ~* "^/(cart|checkout|my-account)(/|\?|$)") { set $skip_cache 1; }
if ($request_uri ~* "^/(wp-admin|wp-json|wp-login\.php|xmlrpc\.php|wp-cron\.php)") { set $skip_cache 1; }
if ($http_cookie ~* "woocommerce_items_in_cart|woocommerce_cart_hash|wp_woocommerce_session_|wordpress_logged_in_|wp-postpass_|comment_author_") { set $skip_cache 1; }
location ~ \.php$ {
include snippets/fastcgi-php.conf; # Debian/Ubuntu; elsewhere use fastcgi_params plus SCRIPT_FILENAME
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_read_timeout 110s; # longer than request_terminate_timeout, shorter than the CDN timeout
fastcgi_cache WOO;
fastcgi_cache_valid 200 301 10m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_cache_background_update on;
fastcgi_cache_lock on;
add_header X-Cache $upstream_cache_status always;
}
fastcgi_cache_bypass stops the response from being taken from the cache and fastcgi_no_cache stops it from being stored; use both together. fastcgi_cache_lock lets only one request at a time go to PHP to populate a page that is not yet in the cache and holds the others back; for expired pages, use_stale updating and background_update keep serving the old copy while a background request refreshes it. By default Nginx does not cache responses that carry a Set-Cookie header. Never add Set-Cookie to fastcgi_ignore_headers: a response that sets a session cookie would enter the cache and be sent to other customers. To test the rules, confirm with curl or a browser that the X-Cache header on the cart page is never HIT, then add a product to the cart and check that category pages now return BYPASS. Replace or extend the cart, checkout and my-account paths with your own slugs.
LiteSpeed
On LiteSpeed or OpenLiteSpeed, use the LiteSpeed Cache plugin rather than hand-written server rules. It understands WooCommerce: the cart, checkout and My Account pages are excluded by default, and there is a WooCommerce tab that appears only while WooCommerce is active. The documentation recommends enabling ESI (Edge Side Includes) with WooCommerce, and the Product Update Interval setting decides how aggressively product and category pages are purged when stock changes. Put extra exclusions in the Do Not Cache URIs and Do Not Cache Cookies fields under Cache > Excludes. If the checkout page is not assigned correctly in WooCommerce settings, the exclusion will not work either.Purging
When stock or prices change, the cached page goes stale. Because the cart and checkout always calculate with current stock and prices, this does not corrupt orders, but a customer may see a sold-out product as in stock. Open-source Nginx has no cache purge directive (fastcgi_cache_purge exists only in the commercial version), so either keep the lifetime short, as in the 10-minute example above, or use a third-party purge module with a plugin that triggers it. LiteSpeed Cache and most managed hosting caches purge the relevant pages themselves when a product is updated.Redis object cache: what it fixes and what it does not
By default the WordPress object cache lives only for the duration of one request. With Redis as a persistent backend, options, metadata, terms, transients and WooCommerce session data stay in memory between requests, which cuts the repeated small queries on uncached cart, checkout, My Account and admin requests. What it does not fix is just as clear: one-off heavy queries (a slow report, an order search using LIKE), waiting on external APIs and heavy PHP code. If Query Monitor shows the time spread across many small, repeated queries, Redis helps; if a handful of slow queries dominate, fix those first.
PHP:
// wp-config.php (above the "That's all, stop editing!" line)
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
define( 'WP_REDIS_DATABASE', 0 ); // give each site its own number if several share one Redis
define( 'WP_REDIS_PREFIX', 'shop1:' ); // prevents key collisions
Code:
# /etc/redis/redis.conf: stop unbounded growth
maxmemory 512mb
maxmemory-policy allkeys-lru
These constants belong to the Redis Object Cache plugin by Till Krüss; after installing it, enable it with wp redis enable and check the connection with wp redis status. On 64-bit systems Redis has no memory limit by default, and the Redis documentation calls allkeys-lru a good default for caches where a subset of keys is read far more often than the rest. If Redis serves only as a cache, persistence to disk is optional. With an object cache in place, the HPOS Data Caching option on the Features screen can cache order data as well; it is off by default, so try it on staging first.
CDN
A CDN's first job is static files. If you also cache HTML at the CDN, copy the same path, cookie and request exclusions into the CDN rule; which conditions are available, such as bypass on cookie, depends on the provider and plan. If you ever see the cart or checkout page served from the CDN with a HIT, switch the rule off at once: that is not a performance issue but a personal data leak.6. Background jobs: WP-Cron and Action Scheduler
WP-Cron is not a real scheduler; it fires due tasks when a page request arrives. When a full-page cache answers most requests without touching PHP, tasks run late, and on busy sites the checks add work to visitor requests. WooCommerce's job queue is Action Scheduler: it runs work such as webhook delivery and Analytics data updates and, since 10.1, maintenance jobs like session cleanup, canceling unpaid orders, scheduled sales and log cleanup. The queue is triggered through WP-Cron once a minute; in addition, while the admin is open, a background run request is dispatched at most once a minute. So if you disable WP-Cron without putting something in its place, the queue only moves when someone opens the dashboard.
PHP:
// wp-config.php: stop page requests from triggering cron
define( 'DISABLE_WP_CRON', true );
Bash:
# Add to the site user's crontab (e.g. crontab -u www-data -e); paths depend on your server
* * * * * cd /var/www/shop && flock -n /tmp/wp-cron-shop.lock /usr/local/bin/wp cron event run --due-now --quiet
# Without WP-CLI (e.g. the cron screen of a shared hosting panel):
# * * * * * wget -q -O /dev/null https://shop.example.com/wp-cron.php
wp cron event run --due-now honors WordPress's doing_cron lock to prevent overlapping runs; flock is an extra safeguard. Check with php -v that the PHP CLI used by cron is the same version as PHP-FPM, otherwise your jobs run on a different PHP with different plugin compatibility. Then check the queue's health once a week:
Bash:
wp action-scheduler status # count per status, oldest and newest dates
wp action-scheduler action list --status=failed --per_page=20 --fields=id,hook,scheduled_date
wp action-scheduler action logs 12345 # log of one failed action
wp action-scheduler run --batch-size=50 --batches=10 # work through a backlog by hand
wp action-scheduler clean --status=complete,canceled --before='14 days ago' --batch-size=500
By default Action Scheduler keeps completed and canceled actions for 31 days and, since version 4.0.0, failed ones for about three months. If wp_actionscheduler_logs has grown very large, shorten the retention period:
PHP:
<?php
// wp-content/mu-plugins/as-retention.php
add_filter( 'action_scheduler_retention_period', function () {
return 14 * DAY_IN_SECONDS; // keep completed and canceled actions for 14 days
} );
The dashboard notice "Action Scheduler: N past-due actions found; something may be wrong." appears when actions are still pending at least one day after their scheduled time. The Action Scheduler documentation treats a few past-due actions as normal; several actions more than a day overdue may point to a problem. Typical causes: WP-Cron was disabled without a system cron replacing it; the server's requests to itself (loopback) are blocked by a firewall, HTTP basic authentication or bot protection; an action's callback throws a fatal error (the action is then marked failed and its log shows the error); or a plugin queued thousands of actions at once. The default runner claims actions in batches of 25, keeps going until it nears a time limit of about 30 seconds or the memory limit, and processes only one batch at a time. The Action Scheduler documentation recommends WP-CLI for large queues because a command-line run is not bound by web request timeouts, so you can work through backlogs with wp action-scheduler run from a separate cron entry.
7. Front end: cart fragments, checkout requests and images
Cart fragments: the classic mini-cart widget sends a ?wc-ajax=get_refreshed_fragments request to keep the cart contents current on cached pages. That request cannot be cached and loads WordPress from top to bottom. Since WooCommerce 7.8 the script is no longer loaded on every page, only when the classic Cart widget is rendered or a theme or plugin pulls it in as a dependency; some themes, Storefront among them, hard-code the widget into their templates. To check, open a cached blog page and look for get_refreshed_fragments in the Network tab. The fix, in order of preference: switch to the Mini-Cart block, which uses the Store API and does not need cart fragments; if you cannot, use the filter from the WooCommerce developer blog so the script runs only on store pages:
PHP:
add_filter( 'woocommerce_get_script_data', function ( $script_data, $handle ) {
if ( 'wc-cart-fragments' === $handle ) {
if ( is_woocommerce() || is_cart() || is_checkout() ) {
return $script_data;
}
return null; // the script still loads but does not run
}
return $script_data;
}, 10, 2 );
Checkout requests: stores set up after the 8.3 release in November 2023 get block-based cart and checkout by default, which talk to the Store API (/wp-json/wc/store/v1/). The classic shortcode checkout sends a ?wc-ajax=update_order_review request whenever the address or shipping choice changes, and shipping rates are recalculated on each one. A shipping plugin that fetches live rates can hold those requests for seconds; check its caching and timeout settings.
Images: WooCommerce uses the woocommerce_thumbnail size for product grids, woocommerce_single (600 pixels wide by default) on product pages and woocommerce_gallery_thumbnail for gallery thumbnails. On classic themes set these under Appearance > Customize > WooCommerce > Product Images; when you save, WooCommerce regenerates the thumbnails in the background on its own (not on multisite, where you can run wp media regenerate instead). The main product image is often the Largest Contentful Paint (LCP) element: do not lazy-load it, and serve it at the right size in a modern format such as WebP or AVIF.
Scripts: page builders, marketing pixels and live chat tools often add scripts to every page. Use Query Monitor's Scripts panel to see what loads where and remove what is not needed. Order Attribution also adds a small tracking script (sourcebuster.js) to every page; keep it if you use the order source data, otherwise you can turn it off on the Features screen.
Core Web Vitals: web.dev's "good" thresholds, measured at the 75th percentile of page loads, are 2.5 seconds for LCP, 200 milliseconds for INP and 0.1 for CLS. Common culprits on product pages are heavy variation pickers and gallery scripts for INP, and late-loading review, badge and installment widgets for CLS.
8. Plugin and theme audit
- Work on staging with a copy of real data.
- Visit the slow pages (product, cart, checkout, order list) with Query Monitor active. In Queries by Component, rank plugins by query count and time; in HTTP API Calls, note every external request and how long it took.
- Deactivate the suspect plugin and repeat the same measurement. If the cause is unclear, switch plugins off half at a time (bisection).
- If you prefer the command line, wp profile stage and wp profile hook --all from the wp-cli/profile-command package answer the same question on the server.
- Remove overlapping plugins: two full-page caches, several security scanners, two SEO or filter plugins doing the same job.
- If the product template uses a heavy page builder, compare the uncached product page time against a template without it.
- Measure live-search plugins that call admin-ajax.php on every keystroke, and anything that calls an external service on add to cart, separately.
9. Scale and safety: staging, search, bots and card testing
Staging and backups: try WooCommerce, PHP and major plugin updates on staging first, and take a backup and test the restore before every data operation such as an HPOS migration, a bulk product import or a table cleanup. WooCommerce's changelog lists many entries tagged Performance in every release; 11.1.0, for example, reduced the number of SQL queries on the cart page for multi-product carts. Staying current is therefore a speed measure in itself.Search on large catalogs: WooCommerce's default product search matches text in the database and can slow down on large catalogs. Moving search and filtering to Elasticsearch with a plugin such as ElasticPress improves both speed and relevance; if you plan to use OpenSearch, check the plugin's compatibility documentation. If order search in the admin is slow, consider the HPOS Full text search indexes option, available as an experimental feature since 9.0 and only with HPOS.
Bots and card-testing attacks: card testing means trying stolen card numbers through small orders. Symptoms include a burst of failed or pending-payment guest orders, heavy POST traffic to checkout endpoints from the same IP ranges and decline notifications from your payment provider. Besides CPU, database and email load, it brings provider fees and account risk. Layer the defenses:
- The Rate limit Checkout option on the Features screen limits the block checkout and the Store API /checkout endpoint to 3 requests per 60 seconds per IP address or user. The classic checkout (?wc-ajax=checkout) is not covered. Because the Store API's proxy support (proxy_support) is off by default, make sure PHP sees the real visitor IP when the site sits behind a CDN (see the real_ip setting below); otherwise the limit counts every customer arriving through the CDN's few addresses together.
- General Store API rate limiting is off by default; switch it on with the woocommerce_store_api_rate_limit_options filter (defaults: 25 requests per 10 seconds).
- Rate-limit sensitive endpoints at the web server (example below) and enable WAF or CDN bot rules.
- A CAPTCHA at checkout (for example Cloudflare Turnstile or reCAPTCHA) works but adds friction for real customers; switching it on during an attack is a reasonable compromise.
- Turn on your payment provider's fraud tools and 3D Secure rules.
Code:
# http {} block: build a key only for sensitive requests (empty keys are not counted)
map $request_uri $woo_sensitive {
default "";
"~wc-ajax=(checkout|add_to_cart|apply_coupon)" $binary_remote_addr;
"~^/wp-json/wc/store(/v[0-9]+)?/checkout" $binary_remote_addr;
}
limit_req_zone $woo_sensitive zone=woo_sensitive:10m rate=20r/m;
# server {} block
limit_req zone=woo_sensitive burst=10 nodelay;
limit_req_status 429;
Treat these values as a starting point and tune them by watching 429 responses in the access log. Mobile carriers and corporate networks can put many users behind one IP address (carrier-grade NAT), so do not set the limits too tight. If the site sits behind a CDN or reverse proxy, recover the real visitor IP with the real_ip module (set_real_ip_from and real_ip_header); otherwise the limit applies to the CDN's addresses and everyone gets blocked together.
Emails: order emails are sent inside the checkout request by default, so a slow SMTP server can hold the order confirmation for seconds. From WooCommerce 10.8, the Deferred emails option on the Features screen, off by default, sends these emails through Action Scheduler outside the request. Make sure Action Scheduler is healthy before you turn it on.
10. Troubleshooting table
| Symptom | Likely cause | Check and fix |
|---|---|---|
| Admin order list and order search are very slow | Orders still in wp_posts and wp_postmeta; compatibility mode left on; meta queries from plugins | wp wc hpos status; migrate to HPOS and turn compatibility mode off after the observation period; find the plugin issuing the query in Query Monitor |
| Checkout spins or times out | Slow shipping, tax or payment API; email sending; PHP-FPM queue full; lock waits on stock rows | Query Monitor HTTP API panel, PHP-FPM slowlog and status page, slow query log; Deferred emails; shorten external API timeouts |
| "N past-due actions found" notice | WP-Cron not running; loopback request blocked; an action throws a fatal error; a sudden burst of jobs | Set up a system cron; wp action-scheduler status; read the logs of failed actions; clear the backlog with wp action-scheduler run |
| High CPU from bots | Crawling of URL combinations with filter and sort parameters; ?add-to-cart= links; floods of wc-ajax requests | Pull the top URLs and IPs from the access log; disallow filter and add-to-cart parameters in robots.txt (only well-behaved bots obey it); rate limits and a WAF for the rest |
| 502 Bad Gateway or 504 Gateway Timeout | pm.max_children exhausted; PHP process killed for lack of memory; mismatched timeouts | max children reached on the status page; OOM entries in dmesg or journalctl; align the timeouts. Behind Cloudflare the same situation shows up as a 524 once the default 125-second wait is exceeded |
| The cache shows another customer's cart or the wrong mini-cart | Cart cookies or paths not excluded; Set-Cookie ignored; the CDN caches HTML | Check the cache header on the cart page with curl -sI (it must never be HIT); fix the rules and purge the whole cache |
| A ?wc-ajax=get_refreshed_fragments request on every page | Classic mini-cart widget; a theme or plugin dependency | Switch to the Mini-Cart block or use the filter in section 7 |
FAQ
Is HPOS safe to enable on an old store?Yes, provided your plugins declare compatibility, you have a backup, you tried it on staging first and you migrate with compatibility mode on. While compatibility mode stays on you can switch back to posts storage instantly. The real risk is custom code that reaches orders through WordPress meta functions or direct SQL; move that code to the WooCommerce CRUD functions before migrating.
Do I need Redis?
No. If most traffic is anonymous and served from the full-page cache, a small store runs fine without it. With logged-in customers, heavy cart and checkout traffic or a large admin team, and with Query Monitor showing time spread across many small repeated queries, Redis makes a clear difference.
Which caching plugin should I use?
The one that fits your server: LiteSpeed Cache on LiteSpeed, server-level FastCGI cache on Nginx (with a helper plugin for purging), or your host's managed cache. The criterion is not the brand but whether it applies the WooCommerce exclusions correctly and purges the right pages when stock or prices change. Never run two full-page caches at once.
Why is checkout slow when the home page is fast?
The home page comes from the cache. Checkout runs in PHP every time: it calculates shipping and tax, often calls external APIs, writes the order and the stock reservation to the database and sends emails. A page cache cannot speed that path up; use Query Monitor and the PHP-FPM slowlog to find which step takes the time.
How many PHP workers do I need?
Multiply your uncached requests per second by the average response time and add headroom for bursts, then cap the result by the memory budget (the RSS you measured per worker) and by what you observe on the CPU. The PHP-FPM status page has the final word: if the queue stays empty and max children reached does not grow, you have enough.
Conclusion: a one-week action list
The order for speeding up WooCommerce is clear: measure first, then make the cheap, low-risk changes such as background jobs and cache rules, and leave the work that needs planning, such as the HPOS migration and a bigger server, for last.- Day 1: fill in the measurement table from section 1; switch on Query Monitor, the slow query log and the PHP-FPM status page.
- Day 2: set DISABLE_WP_CRON and a system cron; review failed and past-due actions with wp action-scheduler status.
- Day 3: verify the full-page cache rules for cart, checkout, My Account and cart cookies; run the HIT and BYPASS tests with curl.
- Day 4: review the PHP version, OPcache and PHP-FPM settings; add the Redis object cache if your measurements call for it.
- Day 5: check the autoload size, expired transients, the sessions table and the largest tables.
- Day 6: record the HPOS status and plugin compatibility; plan the migration on staging.
- Day 7: audit cart fragments, images and plugins; fill in the measurement table again and compare it with day 1.
Last edited: