A bare 429 Too Many Requests tells the client why a request was rejected. It does not tell the client when another attempt has a chance of succeeding. Some clients will retry immediately and send the same traffic straight back.
Return Retry-After when the server can estimate a useful delay. Use it with 429 for rate limiting or with 503 Service Unavailable when a temporary overload or maintenance window has a known recovery time. The value can be a number of seconds or an HTTP date.
ASP.NET Core rate limiters can attach a retry estimate to the rejected lease. Read that estimate in OnRejected instead of copying the configured window into the response:
using System.Globalization;
using System.Threading.RateLimiting;
builder.Services.AddRateLimiter(options =>
{
options.OnRejected = (context, _) =>
{
var response = context.HttpContext.Response;
response.StatusCode = StatusCodes.Status429TooManyRequests;
if (context.Lease.TryGetMetadata(
MetadataName.RetryAfter,
out var retryAfter))
{
var delaySeconds = Math.Ceiling(retryAfter.TotalSeconds);
response.Headers.RetryAfter =
delaySeconds.ToString(CultureInfo.InvariantCulture);
}
return ValueTask.CompletedTask;
};
// Configure the limiter policy here.
});
Rounding up avoids telling the client to return before the calculated delay has elapsed. TryGetMetadata can still fail. A time-based limiter may know when permits replenish, while a concurrency limiter usually cannot predict when active requests will finish. Omit the header when the server has no honest estimate. A hard-coded delay turns a guess into an API contract and can make every client return at the same time.
Treat Retry-After as the earliest sensible retry time, not as permission to repeat any request. Clients should cap their attempts and stay inside the operation’s time budget. If they add jitter, Retry-After remains the floor so the randomized delay never schedules an earlier attempt.
For a state-changing POST, the client needs a reason to believe repetition is safe. That may be an idempotency contract, or evidence that the first request was never applied.
Do not attach Retry-After to a response that cannot succeed unchanged. Validation and authorization failures are obvious examples. Waiting will not change them.