HTTP Status Code'ları ve Soft 404: Google 404, 410, 5xx ve 429'u Nasıl Yorumlar?
Google 404 ve 410'u aynı işler ve URL'yi index'ten çıkarır; 5xx ve 429 ise crawl'ı yavaşlatır, hata sürerse URL'ler düşer. 200 dönen bulunamadı sayfası soft 404'tür. ASP.NET Core'da status code'u koruyan hata sayfası, 503 ve Retry-After, rate limiter, HEAD ve 405, Angular SSR'da RESPONSE_INIT ile gerçek 404 ve mertozen.com'un CMS hatasında 404 yerine 503'e geçişi.
Sürüm notu: Bu yazıdaki kodlar .NET 10 (ASP.NET Core 10) ve Angular 22.2 ile derlenip denenmiştir; Google'ın status code yorumları Google crawling dokümanının Kasım 2026 hâlinden alınmıştır.
Önce Tahmin Et
Bookstore'un kitap detay sayfası, kitabı bulamadığında güzel bir "Üzgünüz, bu kitap bulunamadı" sayfası gösteriyor ve 200 dönüyor. Katalogdan kalıcı olarak çekilen kitaplar için de aynı sayfa çıkıyor. Bir gece veritabanı on dakika cevap vermiyor; o sırada kod, kitabı "bulamadığı" için yine aynı sayfayı 200 ile dönüyor. Ayrıca ekip, crawler'lar sunucuyu yormasın diye kitap sayfalarına IP (Internet Protocol) başına dakikalık bir sınır koymuş ve sınırı aşanlara 429 dönüyor.
Dört soru. Google o "bulunamadı" sayfalarını ne olarak görür? Çekilen kitaplar için 404 mü 410 mu dönmek gerekir, fark eder mi? Veritabanı kesintisi sırasında 200 yerine ne dönülmeli? Ve 429 sınırı Googlebot'a ne yapar?
Önceki yazılarda URL (Uniform Resource Locator), robots.txt, noindex, sitemap ve canonical ile Google'a ne istediğimizi söyledik. Status code ise sunucunun her yanıtta verdiği en temel bilgi: bu URL var mı, taşındı mı, geçici olarak mı çalışmıyor? Google'ın crawl ve index kararlarının çoğu ilk önce bu üç haneli sayıya bakıyor.
Google Her Sınıfı Nasıl Yorumluyor?
Google'ın HTTP (HyperText Transfer Protocol) status code dokümanının özeti:
| Sınıf | Google'ın davranışı |
|---|---|
| 2xx | İçerik index'leme için değerlendirilir; index'lenme garantisi yok. Hata mesajı gösteren 2xx sayfası soft 404 sayılır |
| 3xx | Varsayılan olarak 10 hop'a kadar redirect izlenir; 301/308 güçlü, 302/303/307 zayıf canonical sinyali |
| 4xx (429 hariç) | İçerik yok sayılır; index'teki URL çıkarılır, crawl sıklığı yavaş yavaş düşer. 404 ve 410 aynı şekilde işlenir |
| 429 ve 5xx | Sunucu aşırı yüklü sayılır, crawl geçici olarak yavaşlar; hata sürerse index'teki URL'ler sonunda düşer |
Bu tablodan çıkan üç ders var. Birincisi, 4xx "bu içerik yok" demek ve kalıcı sonuçları var: URL index'ten çıkar. İkincisi, 5xx ve 429 "şu an yok, sonra gel" demek; Google URL'yi bir süre index'te tutar ve crawl hızını düşürür. 500, 502 ve 503'te yavaşlama, hata veren URL sayısıyla orantılı. Üçüncüsü, 429 bir 4xx olsa da Google için bir sunucu hatası sınıfında.
404 ile 410 arasındaki fark sorusunun cevabı da burada: Google dokümanına göre 429 dışındaki bütün 4xx kodları aynı şekilde işleniyor, yani 404 ve 410 Google için aynı. Semantik olarak 410 "kalıcı olarak kaldırıldı" demek ve başka istemciler (cache'ler, link checker'lar) için daha bilgilendirici; Google'dan farklı bir davranış bekleme.
Aralık 2025'te JavaScript SEO (Search Engine Optimization) dokümanına eklenen bir not daha var: 200 dışında status code dönen sayfalar render aşamasına gönderilmeyebilir. Yani 404 sayfanın JavaScript ile ürettiği içerik hiç işlenmeyebilir; bu doğru bir davranış, ama 200 olması gereken bir sayfayı yanlışlıkla 404 dönerken JavaScript'le "kurtarmaya" çalışmanın işe yaramayacağı anlamına geliyor.
Soft 404: 200 Diyen "Bulunamadı" Sayfası
Açılıştaki ilk sorunun cevabı: Google bu sayfaları soft 404 olarak işaretler. Soft 404, 2xx dönen ama içeriği hata mesajı, boş sayfa veya "bulunamadı" olan yanıttır; Search Console'un "Sayfalar" raporunda ayrı bir satır olarak çıkar. Sorun çift taraflı: Google her birini ayrı bir sayfa olarak değerlendirip tekrar tekrar crawl eder, crawl budget'ı boşa gider; sınıflandırma yanlışsa da "bulunamadı" sayfaları index'te kalabilir. Bookstore'un ilk hâli:
// soft 404: 200 diyen bir "bulunamadı" sayfası
app.MapGet("/soft/books/{slug}", (string slug) =>
books.TryGetValue(slug, out string? title)
? Results.Content($"<h1>{title}</h1>", "text/html")
: Results.Content("<h1>Sorry, this book could not be found</h1>", "text/html"));
$ curl -s -i http://localhost:20107/soft/books/neuromancer
HTTP/1.1 200 OK
Content-Type: text/html
<h1>Sorry, this book could not be found</h1>
Doğrusu, kullanıcıya aynı güzel sayfayı gösterip status code'u doğru dönmek. ASP.NET Core'da bunun için endpoint'in yalnız status code döndürmesi, sayfayı ise UseStatusCodePagesWithReExecute'un üretmesi yeterli:
// gövdesiz 4xx/5xx -> /not-found yeniden çalıştırılır, orijinal status code korunur
app.UseStatusCodePagesWithReExecute("/not-found");
Dictionary<string, string> books = new() { ["dune"] = "Dune", ["foundation"] = "Foundation" };
HashSet<string> removed = ["hyperion"]; // katalogdan kalıcı olarak çekildi
app.MapMethods("/books/{slug}", [HttpMethods.Get, HttpMethods.Head], (string slug, HttpContext ctx) =>
{
if (maintenance)
{
// geçici: "sonra tekrar gel", sayfayı index'ten düşürme
ctx.Response.Headers.RetryAfter = "120";
return Results.Text("Back soon", statusCode: StatusCodes.Status503ServiceUnavailable);
}
if (books.TryGetValue(slug, out string? title))
return Results.Content($"<h1>{title}</h1>", "text/html");
return removed.Contains(slug)
? Results.StatusCode(StatusCodes.Status410Gone)
: Results.NotFound();
});
app.Map("/not-found", (HttpContext ctx) =>
Results.Content($"<h1>Page not found</h1><p>Status: {ctx.Response.StatusCode}</p>", "text/html"));
$ curl -s -i http://localhost:20107/books/dune
HTTP/1.1 200 OK
Content-Type: text/html
<h1>Dune</h1>
$ curl -s -i http://localhost:20107/books/hyperion
HTTP/1.1 410 Gone
Content-Type: text/html
<h1>Page not found</h1><p>Status: 410</p>
$ curl -s -i http://localhost:20107/books/neuromancer
HTTP/1.1 404 Not Found
Content-Type: text/html
<h1>Page not found</h1><p>Status: 404</p>
Kullanıcı aynı dostça sayfayı görüyor, crawler doğru kodu alıyor. UseStatusCodePagesWithReExecute yalnız gövdesi boş yanıtlarda devreye giriyor; endpoint kendisi bir gövde yazarsa (yukarıdaki soft 404 gibi) dokunmuyor. Re-execute edilen /not-found handler'ı orijinal status code'u ctx.Response.StatusCode'tan okuyabiliyor; sayfada 404 ve 410 için farklı mesaj göstermek istersen buradan dallanırsın. Results.NotFound() ve diğer IResult tipleri ASP.NET Core Web API (Application Programming Interface) serisinin "ASP.NET Core'da Status Code ve Response: Ok, CreatedAtAction ve ProblemDetails" yazısında.
5xx, 503 ve Retry-After: Geçici Sorunları Geçici Söylemek
Açılıştaki üçüncü soru en pahalısı. Veritabanı cevap vermezken "kitap yok" demek, Google'a o sayfaların yok olduğunu söylemek demek. Kesinti uzun sürerse veya o sırada crawl yoğunsa, var olan sayfalar 404'e düşmüş gibi işlenip index'ten çıkabilir. Geçici bir sorunun doğru dili 503 ve, varsa, bir Retry-After header'ı:
$ MAINTENANCE=1 dotnet run --no-launch-profile --urls http://localhost:20107
$ curl -s -i http://localhost:20107/books/dune
HTTP/1.1 503 Service Unavailable
Content-Length: 9
Content-Type: text/plain; charset=utf-8
Retry-After: 120
Back soon
Google dokümanına göre 5xx alındığında crawl yavaşlıyor ve URL'ler bir süre index'te kalıyor; hata uzun süre devam ederse düşüyor. Bu yüzden 503 kısa kesintiler için doğru araç, günlerce süren bir bakım için değil. Burada dikkat edilecek nokta, hatanın türünü ayırt etmek: "veri kaynağı cevap vermedi" ile "veri kaynağı cevap verdi ve kayıt yok" iki farklı durum ve kodda iki farklı yol olmalı. Repository veya HTTP client katmanının ikisini de null olarak dönmesi, bu ayrımı yok eden klasik tasarım hatası.
429 ve Rate Limiting: Googlebot'u Yavaşlatmanın Bedeli
Bookstore'un rate limiter'ı ASP.NET Core'un yerleşik middleware'iyle kurulu:
builder.Services.AddRateLimiter(o =>
{
o.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
o.OnRejected = (ctx, _) =>
{
ctx.HttpContext.Response.Headers.RetryAfter = "10";
return ValueTask.CompletedTask;
};
o.AddFixedWindowLimiter("books", w => { w.PermitLimit = 3; w.Window = TimeSpan.FromSeconds(10); });
});
$ for i in 1 2 3 4; do curl -s -o /dev/null -w "request $i: %{http_code} retry-after=%header{retry-after}\n" http://localhost:20107/limited/books/dune; done
request 1: 200 retry-after=
request 2: 200 retry-after=
request 3: 200 retry-after=
request 4: 429 retry-after=10
RejectionStatusCode'un varsayılanı 503; burada açıkça 429 seçildi. Açılıştaki dördüncü sorunun cevabı tablodan geliyor: Google 429'u sunucu aşırı yüklü sinyali sayıyor ve crawl'ı yavaşlatıyor; sürekli 429 alan URL'ler ise sonunda index'ten düşebiliyor. Google'ın crawl hızını azaltma rehberi, acil bir aşırı yükte birkaç saatliğine 500, 503 veya 429 dönmeyi öneriyor; ama kalıcı bir rate limit'in Googlebot'a takılması, sitenin kendi crawl'ını kısması demek. Rate limit'i bot trafiğine uygulayacaksan, gerçek Googlebot'u doğrulayıp (serinin on birinci yazısında) ona daha geniş bir sınır tanımak, sahte user-agent'lara ise sıkı davranmak daha doğru. Ayrıca 401 veya 403 ile crawl yavaşlatılamaz; Google bunları 4xx olarak işler ve URL'leri index'ten çıkarır.
HEAD İstekleri ve 405
Minimal API'de MapGet yalnız GET'i eşliyor. Link checker'lar, bazı crawler'lar ve uptime monitörleri ise sayfanın varlığını kontrol etmek için HEAD gönderiyor:
$ curl -sI http://localhost:20107/get-only/books/dune
HTTP/1.1 405 Method Not Allowed
Allow: GET
$ curl -sI http://localhost:20107/books/dune
HTTP/1.1 200 OK
Content-Type: text/html
İkinci endpoint MapMethods ile GET ve HEAD'i birlikte kabul ediyor; Kestrel HEAD yanıtında gövde göndermiyor, status ve header'lar GET ile aynı kalıyor. 405, Google'ın Search için kullandığı GET isteklerini etkilemez, ama HEAD ile kontrol yapan araçlar sayfanın kırık olduğunu raporlar. MVC'de (Model-View-Controller) [HttpGet] attribute'u da aynı şekilde yalnız GET'i eşliyor; çözüm her action'a HEAD eklemek ya da bunu bir convention ile toplu yapmak.
Angular SSR: "Her Şeye 200" Sorunu
SPA'ların (Single-Page Application) klasik sorunu: sunucu her path için aynı index.html'i 200 ile döner, "bulunamadı" kararı tarayıcıda, JavaScript ile verilir. Crawler açısından bu, sitedeki her olası URL'nin var olduğu anlamına gelir. Bookstore'un Angular uygulamasında aynı component'i RenderMode.Client ile servis eden route bunu birebir gösteriyor:
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:20110/client/books/hyperion
200
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:20110/books/hyperion
404
İkinci route SSR (Server-Side Rendering) ile çalışıyor ve component, kitabı bulamadığında status code'u sunucuda ayarlıyor. Angular'ın RESPONSE_INIT token'ı, render sırasında yanıtın status ve header'larına erişim veriyor; token yalnız sunucuda request sırasında dolu, build'de, prerender'da ve tarayıcıda null:
export class BookDetail {
readonly slug = input.required<string>();
protected readonly book = computed(() => BOOKS.find(b => b.slug === this.slug()));
private readonly seo = inject(SeoService);
private readonly responseInit = inject(RESPONSE_INIT, { optional: true });
constructor() {
effect(() => {
const b = this.book();
if (b) {
this.seo.update({ title: b.title, description: b.description, path: `/books/${b.slug}` });
} else {
// bilinmeyen slug: sunucuda gerçek 404, tarayıcı için noindex
if (this.responseInit) {
this.responseInit.status = 404;
}
this.seo.update({ title: 'Not found', description: '', path: `/books/${this.slug()}`, noindex: true });
}
});
}
}
Hiçbir route'a uymayan path'ler için ise status'u server route'ta sabit vermek yeterli:
{ path: '**', renderMode: RenderMode.Server, status: 404 },
$ curl -s -D - -o /dev/null http://localhost:20110/nope | head -1
HTTP/1.1 404 Not Found
Kitap route'u RenderMode.Prerender ile build anında bilinen üç kitap için HTML (HyperText Markup Language) üretiyor; listede olmayan bir slug geldiğinde Angular varsayılan olarak o isteği sunucuda render ediyor ve 404 kararı orada veriliyor. Tamamen client-side çalışmak zorunda olan bir uygulamada Google'ın önerisi iki seçenek: bilinmeyen içerikte JavaScript ile sunucunun 404 döndüğü bir URL'ye yönlendirmek ya da JavaScript ile noindex eklemek. İkisi de render'a bağımlı ve SSR'daki gerçek 404 kadar güvenilir değil.
mertozen.com'da Status Code'lar
Bu sitenin kaynak kodunun son iki sürümü tam bu yazının konusunu düzeltiyor. 7 Kasım 2026'daki gözlemde siteye gönderilen HEAD istekleri 405 dönüyordu; v1.12.1 sürümünde GET kabul eden her MVC action'ına HEAD'i de ekleyen bir IActionModelConvention (HeadMatchesGetConvention) eklendi. Commit mesajındaki gerekçe: link kontrol araçları ve crawler'lar 405 alıyordu.
v1.12.2'deki düzeltme daha önemli. Site içeriğini bir CMS'in (Content Management System) API'sinden okuyor; daha önce CMS geçici olarak hata döndüğünde blog detay action'ı yazıyı "bulamadığı" için 404 dönüyordu. Yani CMS'teki birkaç dakikalık bir aksama, crawl'a denk gelirse var olan yazıları Google'a "yok" diye bildirebiliyordu. Yeni sürümde CMS hatası (429, 5xx veya timeout) ayrı bir exception olarak yukarı taşınıyor ve bir middleware bunu 503, Retry-After: 30, Cache-Control: no-store ve sayfa içinde noindex ile karşılıyor; "kayıt gerçekten yok" durumu ise 404 olarak kalıyor. Açılıştaki veritabanı kesintisi senaryosunun gerçek hayattaki karşılığı ve çözümü bu.
404 tarafında site UseStatusCodePagesWithReExecute("/404") kullanıyor; gövdesiz her 4xx/5xx yanıtı dostça bir sayfayla, orijinal status code korunarak dönüyor ve o sayfa ayrıca noindex, follow taşıyor. Yani yukarıda kurduğumuz yapının aynısı.
Üretimde Dikkat
Search Console'un "Sayfalar" raporunda "Soft 404" satırını izle. Bu satırda gerçekten var olan sayfalar görüyorsan, sayfa içeriğin Google'a hata sayfası gibi görünüyor demektir (çok az içerik, "bulunamadı" benzeri bir metin); boş kategori sayfaları sık görülen bir örnek.
Crawl Stats raporunda (Search Console ayarları altında) yanıt koduna göre dağılıma ve sunucu yanıt süresine bak. 5xx oranındaki bir artış ve ardından crawl isteklerindeki düşüş, Google'ın sunucunu korumak için yavaşladığının işareti.
Hata yolunu test et. "Kayıt yok" ve "veri kaynağı cevap vermedi" durumları için ayrı integration test'ler yaz; ikincisinde 503 döndüğünü, birincisinde 404 döndüğünü assert et. Bu iki durumun karışması, testi olmayan kodda üretimde ancak trafik düştükten sonra fark edilir.
Uzun bakımlarda 503'ü günlerce dönme. Google hata sürdükçe URL'leri düşürüyor; bakım uzunsa okunur bir bakım sayfasını 200 ile sunmak ya da sitenin salt okunur bir kopyasını ayakta tutmak daha az risklidir.
Sık Düşülen Tuzaklar
"Bulunamadı" sayfasını 200 ile dönmek. Soft 404 olur; Google'ın vaktini harcar ve sınıflandırması sana bağlı değildir.
Geçici veri kaynağı hatasını 404'e çevirmek. Var olan sayfalar Google'a yok gibi görünür.
Silinen her sayfayı ana sayfaya 301 ile yönlendirmek. Google'ın site taşıma rehberi çok sayıda sayfayı alakasız bir hedefe (ör. ana sayfa) yönlendirmekten kaçınmayı söylüyor; bu tür redirect'ler soft 404 olarak raporlanabilir. İlgili bir yeni sayfa yoksa 404 veya 410 dön. Redirect'lerin doğru kullanımı bir sonraki yazıda.
Crawl'ı 401, 403 veya kalıcı bir 429 ile kısmaya çalışmak. 401/403 URL'leri index'ten çıkarır; sürekli 429 da sonunda aynı sonuca gider.
SPA'da bilinmeyen route'lar için 200 dönmek. SSR veya prerender kullanıyorsan ** route'una status: 404 ver, dinamik içerikte RESPONSE_INIT ile status ayarla.
Küçük Bir Deneme
★☆☆ Kendi sitende var olmayan üç URL iste (curl -s -o /dev/null -w "%{http_code}"): rastgele bir path, var olmayan bir slug, var olmayan bir dosya uzantısı. Hepsi 404 mü?
★★☆ Bookstore'un kitap endpoint'ini bir IBookCatalog service'ine bağla; service "bulunamadı" için null, "kaynak cevap vermedi" için özel bir exception dönsün. Exception'ı 503 ve Retry-After'a çeviren bir middleware yaz ve iki durumu WebApplicationFactory ile test et.
★★★ Rate limiter'ı user-agent'a ve doğrulanmış bot durumuna göre partition'la: doğrulanmamış "Googlebot" user-agent'ları sıkı bir sınıra, gerçek Googlebot daha geniş bir sınıra, diğer istemciler varsayılan sınıra düşsün. Doğrulama fonksiyonunu serinin on birinci yazısından sonra gerçeğiyle değiştirmek üzere şimdilik bir interface arkasına koy.
Status code ailesinin ortasındaki 3xx'i bilinçli olarak sona bıraktık. Bir sonraki yazıda 301, 302, 307 ve 308'in farkını, redirect'lerin canonical'a etkisini, zincirleri ve ASP.NET Core'un URL Rewriting Middleware'i ile veritabanından beslenen eski slug → yeni slug redirect'lerini kuracağız.