# Bir çekirdek Go'da 14.330, PHP-FPM'de 5.152 OAuth2 isteği taşıyor

> Aynı OAuth2 + PostgreSQL API'si Go, PHP-FPM ve FrankenPHP ile 1/2/4 çekirdekte karma okuma-yazma altında ölçüldü: çekirdek başına kapasite ve istek başına CPU.

- Tür: Ölçüm
- Soru: Her istekte RS256 bearer token doğrulayıp PostgreSQL'e bir satır yazan ya da okuyan aynı API, bir, iki ve dört çekirdekte Go, PHP-FPM ve FrankenPHP worker ile saniyede kaç karma istek taşıyor?
- Bulgu: Dört çekirdekte Go 57.321, FrankenPHP 25.659, PHP-FPM 20.606 karma istek taşıdı; çekirdek başına 14.330, 6.415 ve 5.152. Kapasite planına giren sayı bu değil, istek başına uygulama CPU'su: Go 66,8, FrankenPHP 110,7, PHP-FPM 187,7 mikrosaniye. FrankenPHP doyduğunda dört çekirdeğin ancak 2,84'ünü kullanabiliyor — Go 3,83, PHP-FPM 3,87. Darboğaz veritabanı değil: aynı dört çekirdekte PostgreSQL tek başına saniyede 68.212 satır yazıyor, yani en hızlı adayın karma tavanının üstünde.
- Yöntem: Aynı sözleşme üç kez yazıldı ve her istekte RS256 bearer token doğrulandı (imza, iss, aud, exp, nbf, scope). Adaylar Go 1.27.1 (net/http, pgx, golang-jwt), nginx + php-fpm üzerinde PHP 8.5.10 ve FrankenPHP 1.12.7 worker modunda PHP 8.5.10; framework yok, iki PHP adayı aynı sınıfı çalıştırıyor. Yük karma: iki open-loop üreteç aynı anda, biri okuyup biri yazarak, her biri kendi access token'ıyla. Her aday 1, 2 ve 4 sabitlenmiş çekirdekte ve o çekirdek bütçesindeki kendi en iyi havuz boyutuyla ölçüldü; havuz boyutu ayrı bir fazda 8/16/32/64 süpürülerek seçildi. Yayınlanan sayı kapalı çevrimde doyum tavanıdır: hücre başına 5 tekrar, her tekrardan önce uygulama kodu çalıştırmayan bir nginx aynı çekirdeklerde beklenen hızın %115'iyle sınandı ve 45 tekrarın 45'i bu kapıdan geçti, en iyi tekrar raporlandı. CPU ve bellek cgroup sayaçlarından, ana makine yükü her tekrarda ayrıca kaydedildi. Koşunun tamamı 333 yük ölçümü ve 110.617.180 yanıt; yayınlanan tavan fazı bunların 45'i ve 18.551.833 yanıtı. Hiçbir ölçümde non-2xx yanıt ya da transport hatası görülmedi.
- Metrikler: Çekirdek başına kapasite · Go: 14.330 istek/sn · Çekirdek başına kapasite · PHP-FPM: 5.152 istek/sn · İstek başına CPU · Go → PHP-FPM: 66,8 → 187,7 µs · FrankenPHP · doyumda çekirdek kullanımı: %71 · PostgreSQL · kendi INSERT tavanı: 68.212 satır/sn
- Ölçüm tarihi: 2026-09-17
- Güven: Orta güven
- Durum: Yürürlükte
- Program: Servis & yük
- Ortam: Go 1.27.1 · net/http · pgx 5.11.0 · golang-jwt 5.3.1 · PHP 8.5.10 · opcache açık · JIT kapalı · firebase/php-jwt 7.1.1 · PHP-FPM nginx 1.26.3 + php-fpm aynı konteynerde · pm=static · FrankenPHP 1.12.7 (Caddy 2.11.4) · worker modu · ZTS · Veritabanı PostgreSQL 17.11 · 1.000.000 satır · synchronous_commit açık · Yük üreteci oha 1.15.0 · iki eşzamanlı üreteç · 64 bağlantı/üreteç · Donanım Apple M4 Pro · 12 çekirdek · 24 GB · macOS 27.0 · Sanallaştırma Docker Desktop 29.8.0 · 12 vCPU / 7,75 GB · aarch64 · Çekirdek ayrımı aday 0-3 · PostgreSQL 4-7 · yük 8-11 · Tahminci hücre başına 5 tekrar, üreteç yoklamasından geçenlerin en iyisi
- Teknolojiler: Go, PHP, PostgreSQL, Docker, nginx
- Tekrarlamak için: ./bench/build.sh && ./bench/verify.sh && STAMP=$(date -u +%F) ./bench/capacity.sh --all
- Kaynak kodu: https://github.com/muhammetsafak/php-go-bench
- Ham veri: https://github.com/muhammetsafak/php-go-bench/tree/main/results/capacity-2026-09-17
- Ham veri lisansı: https://github.com/muhammetsafak/php-go-bench/blob/main/LICENSE
- Yayın: 2026-09-18
- Kaynak: https://www.muhammetsafak.com.tr/research/go-php-oauth2-cekirdek-basina-kapasite/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
Saniyede on binlerce isteği karşılayacak bir OAuth2 servisi için Go önerdiğimde
ekibin sorusu haklıydı: ne kadar kazanıyoruz? "Go daha hızlı" cevabı bu soruyu
karşılamıyor, çünkü kapasite tablosuna yazılan şey saniyede istek değil,
**çekirdek**. Bu yüzden aynı API'yi üç kez yazdım ve üçünü de bir, iki ve dört
çekirdekte, yarısı okuma yarısı yazma olan aynı trafiğin altında ölçtüm. Ham
koşuların hepsi [php-go-bench](https://github.com/muhammetsafak/php-go-bench)
deposunda duruyor.

## Ne ölçtüm

Üç uç nokta, üç adayda da birebir aynı sözleşmeyle çalışıyor. Hepsi önce bearer
token'ı doğruluyor: RS256 imzası, `iss`, `aud`, `exp`, `nbf` ve uç noktaya göre
`scope`. Doğrulama hiçbir adayda önbelleğe alınmıyor.

| Aday | HTTP | Veritabanı idiom | Havuz boyutu (1c / 2c / 4c) |
| --- | --- | --- | --- |
| Go | net/http | pgx, bağlantı başına statement önbelleği | 16 / 64 / 64 |
| FrankenPHP (worker) | Caddy, gömülü | PDO, worker ömrü boyunca hazırlanmış statement | 8 / 8 / 8 |
| PHP-FPM | nginx, aynı çekirdek bütçesinde | PDO, tek gidiş-dönüşlük parametreli çağrı | 8 / 16 / 32 |

Framework yok. İki PHP adayı aynı Api.php sınıfını çalıştırıyor, yalnız giriş dosyaları farklı. Her aday, süreç ya da istek ömrünün izin verdiği en hızlı veritabanı idiom'unu kullanıyor.

Yük karma: iki open-loop üreteç aynı anda koşuyor, biri `GET /events/{id}` ile
milyon satırlık tablodan birincil anahtarla tek satır okuyor, diğeri
`POST /events` ile tek `INSERT … RETURNING` yazıyor. İkisi de kendi access
token'ını gönderiyor.

**Burada yayınlanan "kapasite" bir doyum tavanıdır, bir servis seviyesi
değildir.** Yani adayın yanıtları yetiştirdiği en yüksek hızdır; "p99 şu eşiğin
altında kalarak" güvencesi taşımaz. Bunu ayrıca ölçmeye üç kez çalıştım ve
üçünde de bu tezgâhta güvenilmez sonuç aldım; nedeni aşağıda, sınırlar bölümünde.
Tavanın yanına o tavandaki gecikmeyi de bastım, böylece sayının neye mal
olduğunu görebilirsiniz.

## Çekirdek başına kapasite

**Karma OAuth2 trafiğinde doyum tavanı**

Her sütun, o adayın o çekirdek bütçesinde yetiştirdiği toplam istek/sn. Okuma ve yazma birlikte.

Kaynak: Hücre başına 5 tekrar, üreteç yoklamasından geçenlerin en iyisi — bench/capacity-report.mjs

|  | 1 çekirdek | 2 çekirdek | 4 çekirdek |
| --- | --- | --- | --- |
| Go | 16883 | 29449 | 57321 |
| FrankenPHP (worker) | 6544 | 14306 | 25659 |
| PHP-FPM | 6218 | 11211 | 20606 |

Dört çekirdekte Go, PHP-FPM'in **2,78 katını**, FrankenPHP'nin 2,23 katını
taşıyor. Çekirdek başına çevirdiğinizde fark daha okunur hâle geliyor: Go
14.330, FrankenPHP 6.415, PHP-FPM 5.152 istek.

Üçü de çekirdek eklendikçe doğrusala yakın ölçekleniyor ama hiçbiri tam
doğrusal değil. Bir çekirdekten dörde çıkarken Go teorik dördün %84,9'unu,
PHP-FPM %82,8'ini alıyor. FrankenPHP'nin %98'i yanıltıcı: paydası, tek çekirdekte
zaten düşük olan kendi rakamı.

| Aday | Tavan (4c) | Çekirdek başına | Tavandaki p50 (okuma/yazma) | Tavandaki p99 | Tepe RSS |
| --- | --- | --- | --- | --- | --- |
| Go | 57.321/sn | 14.330 | 1,81 / 2,42 ms | 4,69 / 5,41 ms | 30,4 MiB |
| FrankenPHP (worker) | 25.659/sn | 6.415 | 4,77 / 4,96 ms | 8,80 / 9,06 ms | 60,3 MiB |
| PHP-FPM | 20.606/sn | 5.152 | 6,03 / 6,24 ms | 8,27 / 8,45 ms | 60,0 MiB |

Doyum tavanındaki gecikme — yani adayın en zorlandığı andaki hâli. Go tavanını gecikmeyle satın almıyor: en yüksek hızı verirken aynı zamanda en düşük p99'u veriyor.

## Tabloya yazacağınız sayı bu değil

Dört çekirdekteki 57.321 yalnız dört çekirdek için doğru. Kendi hedefinize
taşıyan sayı çekirdek başına istek değil, **istek başına CPU**:

**İstek başına uygulama CPU'su, doyum tavanında**

Adayın cgroup CPU sayacı, cevaplanan istek sayısına bölündü. Veritabanının CPU'su dahil değil.

Kaynak: Doyum tavanı koşuları, en iyi geçerli tekrar

|  | 1 çekirdek | 2 çekirdek | 4 çekirdek |
| --- | --- | --- | --- |
| Go | 57.8 µs | 63.65 µs | 66.77 µs |
| FrankenPHP (worker) | 82.77 µs | 100.68 µs | 110.74 µs |
| PHP-FPM | 157.07 µs | 173.83 µs | 187.74 µs |

Bu sayı hedef hızla çarpıldığında gereken çekirdeği veriyor. Saniyede 30.000
karma istek için Go'da hesap 30.000 × 66,8 µs = **2,0 çekirdek**, FrankenPHP'de
3,3, PHP-FPM'de 5,6.

Üç adayda da istek başına CPU çekirdek sayısı arttıkça **yükseliyor** — Go'da 57,8'den
66,77'ye, PHP-FPM'de 157,07'den 187,74'e. Bu, ölçeklemenin neden doğrusal olmadığının
sayaç tarafındaki karşılığı: eşzamanlılık arttıkça bağlam değişimi, kilit ve önbellek
kaçırma maliyeti istek başına payı büyütüyor. Planlama yaparken hedeflediğiniz
instance boyutunun sütununu kullanın, tek çekirdeğin sayısını dörtle çarpmayın.

## FrankenPHP'nin kullanamadığı çekirdekler

FrankenPHP'nin istek başına CPU'su PHP-FPM'inkinin %41 altında (110,74'e karşı 187,74 µs). Bu tasarruf
kapasiteye tam dönüşmüyor, çünkü doyduğunda çekirdeklerini bitiremiyor:

| Aday | 1 çekirdekte kullanılan | 2 çekirdekte | 4 çekirdekte |
| --- | --- | --- | --- |
| Go | 0,98 (%98) | 1,87 (%93,5) | 3,83 (%95,8) |
| PHP-FPM | 0,98 (%98) | 1,95 (%97,5) | 3,87 (%96,8) |
| FrankenPHP (worker) | 0,54 (%54) | 1,44 (%72) | 2,84 (%71) |

Doyum anında adayın cgroup'unun gerçekten harcadığı çekirdek. Go ve PHP-FPM verilen bütçeyi bitiriyor; FrankenPHP tek çekirdekte yarısını, dört çekirdekte 1,16'sını boşta bırakıyor.

Tek çekirdekte FrankenPHP, PHP-FPM'den yalnız %5 daha fazla istek taşıyor
(6.544'e karşı 6.218) — oysa istek başına CPU'su neredeyse yarısı. Aradaki
kazancın tamamı boşta kalan çekirdekte kayboluyor. İki ve dört çekirdekte
darboğaz gevşiyor ve fark açılıyor (%28 ve %25), ama üçte birlik bütçe hâlâ
kullanılmıyor.

Darboğazın nerede olduğunu bu kayıt ölçmedi. Havuz boyutu değil: 8, 16, 32 ve 64
worker denendi ve FrankenPHP her çekirdek bütçesinde **8**'i seçti, yani daha
fazla eşzamanlılık ona yardım etmiyor. Caddy ile PHP thread'leri arasındaki
geçişte ya da ZTS derlemesinde olabilir. Pratik sonuç şu: FrankenPHP'ye geçmek
aynı işi daha az CPU zamanıyla yapıyor, ama ödediğiniz çekirdeğin üçte birini
size geri vermiyor.

## Darboğaz veritabanı değil

FrankenPHP'nin boşta bıraktığı 1,16 çekirdek uygulama tarafında. Peki aynı yükü
altta taşıyan PostgreSQL nerede duruyor?

| Ölçüm | Değer | Kullanılan çekirdek (4 üzerinden) |
| --- | --- | --- |
| PostgreSQL tek başına, pgbench INSERT | 68.212 satır/sn | 3,45 |
| Go doyarken veritabanının yaptığı iş | 24.930 yazma + 32.392 okuma/sn | 2,34 |
| FrankenPHP doyarken | 12.575 yazma + 13.084 okuma/sn | 1,11 |
| PHP-FPM doyarken | 10.128 yazma + 10.478 okuma/sn | 1,46 |

pgbench satırı HTTP ve token olmadan, aynı satırı aynı tabloya aynı dayanıklılık ayarıyla yazıyor. Üç adayın hiçbiri veritabanını doyurmuyor.

En hızlı aday tavanındayken bile PostgreSQL dört çekirdeğinin 2,34'ünü
kullanıyor ve kendi tavanı o noktanın epey üstünde. Yani bu iş yükünde seçim
tamamen uygulama runtime'ında: veritabanını değiştirmek kapasite tablosunu
kıpırdatmıyor, dili değiştirmek 2,78 kat kıpırdatıyor.

Bir ayrıntı yine de veritabanı tarafında görünüyor. İstek başına PostgreSQL
CPU'su Go'da 40,75 µs, FrankenPHP'de 43,2 µs, PHP-FPM'de 71,07 µs. Statement'ı
tekrar kullanabilen iki aday birbirine yakın; kullanamayan PHP-FPM veritabanına
istek başına %74 daha fazla iş yaptırıyor. Muhtemel sebep her sorgunun yeniden
ayrıştırılıp planlanması; bu kayıt bunu ayrıca ölçmedi.

## Her adaya kendi en iyi havuzu

Bu tür karşılaştırmalara gelen ilk itiraz "PHP'yi kötü ayarlamışsın" olur. Bu
yüzden havuz boyutu ölçümün öncesinde ayrı bir faz olarak süpürüldü: her aday,
her çekirdek bütçesinde 8, 16, 32 ve 64 ile ikişer kez tam gaz koşturuldu ve
kendi en iyisiyle ölçüme girdi. Havuz boyutu aynı zamanda veritabanı bağlantı
tavanıdır, yani üç adayda da tek bir düğme.

| Aday | 1 çekirdek | 2 çekirdek | 4 çekirdek | En kötü seçimin bedeli |
| --- | --- | --- | --- | --- |
| Go | 16 | 64 | 64 | %70,6 (4c: 8 worker 32.666, 64 worker 55.715) |
| PHP-FPM | 8 | 16 | 32 | %30,7 (1c: 64 worker 4.734, 8 worker 6.188) |
| FrankenPHP (worker) | 8 | 8 | 8 | %29,6 (2c: 32 worker 11.252, 8 worker 14.580) |

Seçilen havuz boyutları ve o hücrede en iyi ile en kötü seçim arasındaki fark. Üç adayda da en yüksek throughput'u veren boyut aynı zamanda en iyi p99'u verdi, yani seçim ölçütü gecikmeyi feda etmiyor.

Sabit bir sayı üçünden hiçbirinin en iyisi değil ve yön de aynı değil: Go
çekirdek arttıkça daha fazla eşzamanlılık istiyor, iki PHP adayı istemiyor.
Tek çekirdekte 64 worker'lı php-fpm, 8 worker'lıdan **%24 daha az** iş çıkarıyor.

## Hedefinizi çekirdeğe çevirmek

| Hedef hız | Go | FrankenPHP | PHP-FPM |
| --- | --- | --- | --- |
| 10.000 istek/sn | 1 | 1 | 1 |
| 20.000 istek/sn | 1 | 1 | 1 |
| 30.000 istek/sn | 1 | 2 | 2 |
| 50.000 istek/sn | 1 | 2 | 3 |
| 100.000 istek/sn | 2 | 4 | 5 |

Dört çekirdekli instance sayısı, ölçülen tavana bölünüp yukarı yuvarlanarak. Yatay ölçeklemenin doğrusal olduğunu ve baş üstü boşluk bırakmadığınızı varsayıyor; veritabanı çekirdekleri dahil değil.

> **Sonuç**
>
> Hedefiniz saniyede 20.000 karma isteğin altındaysa üç aday da tek bir dört
> çekirdekli instance'a sığıyor ve runtime seçimi kapasite tablonuzda bir satır
> bile değil. 30.000'in üstünde fark makineye dönüşüyor, 100.000'de Go'ya
> ayıracağınız iki instance'a karşı PHP-FPM'e beş instance gerekiyor. Kendi
> hesabınız için: hedef hız × istek başına CPU = çekirdek; FrankenPHP'de buna
> boşta kalan üçte biri de ekleyin.

## Nasıl ölçtüm

- **Bütçe.** Docker VM'inin 12 vCPU'su ayrık üç kümeye bölündü: aday 0-3,
  PostgreSQL 4-7, yük üreteci 8-11. php-fpm'in nginx'i de adayın bütçesinin
  içinde; Go ve FrankenPHP HTTP'yi kendileri karşılıyor. Bellek her çekirdek
  bütçesinde 1 GiB sabit, böylece eksen yalnız çekirdek.
- **Her hücre aynı yerden başlıyor.** Her hücre, tohumlanmış veritabanının
  `CREATE DATABASE … TEMPLATE` ile alınmış bir kopyasıyla başlıyor; kopya
  `pg_prewarm` ile önbelleğe çekiliyor, `CHECKPOINT`
  alınıyor ve taze bir aday konteyneri açılıp ısıtılıyor.
- **Üreteç yoklaması.** Her tekrardan önce, uygulama kodu çalıştırmayan bir
  nginx adayın kendi çekirdeklerine konuyor ve aynı iki üreteçle adayın
  beklenen tavanının %115'i isteniyor. Yoklamanın altında kalan tekrar adayı
  değil üreteci ölçmüş olur ve elenir. 45 tekrarın 45'i geçti.
- **Tahminci en iyi tekrar.** Ölçüm bir dizüstünde koşuyor ve makinedeki her iş
  ölçümden düşüyor; girişim yalnız aşağı çekebilir. Bu yüzden her tekrarın
  yanına macOS'un o anki yükü de kaydedildi.
- **Bağımsız doğrulama.** Aynı tavanlar, on iki saat önce farklı bir bağlantı
  sayısıyla koşan havuz süpürmesinde de ölçüldü ve iki bağımsız ölçüm birkaç
  yüzde içinde örtüştü.

## Sınırlar ve dürüstlük notları

1. **Servis seviyeli kapasite ölçülemedi.** "p99 10 ms'nin altında kalarak en
   fazla kaç istek" sorusunu üç ayrı yöntemle ölçmeye çalıştım; üçü de bu
   tezgâhta güvenilmez çıktı ve elendi. Sebep, open-loop ölçümde sınırın bir
   eğim değil **uçurum** olması: servis yavaşlayınca yük üreteci takvimden geri
   kalıyor ve gecikme düzeltmesi bunu üste yazıyor; uçurumun yeri de tekrarlar
   arasında geziyor. Aynı hücrenin tekrarları beş kata kadar ayrıştı. Elenen üç
   denemenin ham verisi ve teşhisi
   [EXCLUDED.md](https://github.com/muhammetsafak/php-go-bench/blob/main/results/capacity-2026-09-17/EXCLUDED.md)
   dosyasında duruyor. Bu yüzden yayınlanan sayı doyum tavanıdır ve `confidence`
   alanı `medium`.
2. **Tek makine, dizüstü.** 12 vCPU'nun tamamı Docker VM'ine verildiği için
   macOS'ta koşan her iş doğrudan ölçümden düşüyor. Bu bir varsayım değil,
   ölçülmüş bir olay: bir koşu sırasında ana makinede headless Chromium
   süreçleri on çekirdeğin üstünü kullandı ve aynı Go servisi 57.321 yerine
   17.490 istek/sn ölçüldü. O koşu atıldı, yoklama kapısı ve ana makine yükü
   kaydı bu yüzden eklendi.
3. **Karışım %50/%50.** Kendi trafiğiniz okuma ağırlıklıysa üç adayın da
   tavanı yükselir, çünkü okuma, yazmadan ucuz. Oran sizin profilinizle
   değişir; istek başına CPU'yu okuma ve yazma için ayrı ayrı ölçmek bu
   kaydın kapsamında değildi.
4. **Disk garantisi doğrulanmadı.** Docker Desktop VM diskindeki `fsync`'in
   çıplak donanımla aynı garantiyi verip vermediğini kontrol etmedim. Mutlak
   yazma rakamları iyimser olabilir; üç adayın karşılaştırması etkilenmiyor,
   çünkü üçü aynı veritabanına yazıyor.
5. **Kapsam dışı.** Framework'ler, PHP JIT, Swoole ile RoadRunner, PostgreSQL'e
   alternatif veritabanları, token üreten authorization server ve kodu Go'ya
   taşımanın ekip maliyeti ölçülmedi. Framework'lerin kendi maliyeti
   [PHP framework yük testi](/research/php-framework-yuk-testi/) kaydında.

Kendi sayınızı üretmek için
[php-go-bench](https://github.com/muhammetsafak/php-go-bench) deposunu kendi
token biçiminiz ve kendi sorgunuzla, kendi donanımınızda koşturun. Tablonuza
yazacağınız değer istek başına CPU; gerisi çarpma.
