Start Debugging

Fix: "413 Request Entity Too Large" when uploading a file to an ASP.NET Core endpoint

Kestrel caps request bodies at 30,000,000 bytes by default and returns 413 when you exceed it. Raise MaxRequestBodySize globally, per endpoint, or in web.config behind IIS.

An upload returns 413 Request Entity Too Large because Kestrel caps every request body at 30,000,000 bytes (about 28.6 MiB) by default, and your file is bigger. Raise the limit where it actually applies: set MaxRequestBodySize on Kestrel globally, apply [RequestSizeLimit] or [DisableRequestSizeLimit] on a controller action, bump FormOptions.MultipartBodyLengthLimit for form posts, and behind IIS raise maxAllowedContentLength in web.config. Verified against ASP.NET Core 11 on .NET 11 with C# 14; the limits and defaults are identical from .NET 8 through .NET 11.

The error in context

There are two ways this surfaces. The first is the HTTP response the client sees:

HTTP/1.1 413 Request Entity Too Large
Content-Length: 0
Connection: close
Date: Fri, 17 Jul 2026 10:04:11 GMT

The second is the log line Kestrel writes on the server, which is the one that tells you exactly which limit fired:

Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException:
Request body too large. The max request body size is 30000000 bytes.

That 30000000 is the tell. If you have not changed anything, the number in the message is the framework default, and the fix is to raise or remove the limit on the layer that owns it. If the number is something else, someone already configured a limit and you are hitting it. Either way the cause is the same: the request body exceeded a configured maximum, and the server rejected the connection before your handler could finish reading it.

Why this happens

An upload can be blocked by four different limits, and they live in different places. Getting the fix right means knowing which one produced your 413.

If the request works against Kestrel directly but fails behind a proxy or IIS, the limit you need to change is not in your C# code. Check the outermost layer first.

Minimal repro

The smallest endpoint that reproduces the default 413. No custom limits, just the framework defaults:

// .NET 11, ASP.NET Core 11, C# 14 -- Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/upload", async (HttpRequest request) =>
{
    // Reading the body is what trips the limit
    using var reader = new StreamReader(request.Body);
    var content = await reader.ReadToEndAsync();
    return Results.Ok(new { bytes = content.Length });
});

app.Run();

Post anything larger than 30,000,000 bytes and the read throws:

# .NET 11 -- generate a 40 MB file and POST it
head -c 40000000 /dev/urandom > big.bin
curl -i -X POST http://localhost:5000/upload \
  -H "Content-Type: application/octet-stream" \
  --data-binary @big.bin
# -> HTTP/1.1 413 Request Entity Too Large

The 40 MB payload clears the 128 MiB multipart limit (it is not multipart anyway), but it blows past the 30 MB Kestrel body cap, so you get 413, not 400.

Raise the limit where it applies

Work through these based on where your 413 is coming from. Most self-hosted Kestrel apps only need the first one.

1. Raise or remove the Kestrel global limit

Configure KestrelServerOptions.Limits.MaxRequestBodySize at startup. Set it to a specific byte count, or to null to remove the cap entirely:

// .NET 11, ASP.NET Core 11, C# 14 -- Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    // 200 MB ceiling for the whole app
    options.Limits.MaxRequestBodySize = 200 * 1024 * 1024;
});

var app = builder.Build();

Prefer a real ceiling over null. An unbounded body size is a denial-of-service vector: one client can stream gigabytes into your process and exhaust memory or disk. Pick the largest legitimate upload you expect and add headroom, rather than disabling the guard.

2. Set a per-endpoint limit on controllers with attributes

If only one endpoint accepts large uploads, do not raise the global limit for the whole app. On a controller action or Razor Page, use [RequestSizeLimit(bytes)] to set a specific cap, or [DisableRequestSizeLimit] to remove it for that action only. Both attributes implement IRequestSizeLimitMetadata, which the MVC pipeline reads per request:

// .NET 11, ASP.NET Core 11, C# 14
[ApiController]
[Route("api/[controller]")]
public sealed class FilesController : ControllerBase
{
    [HttpPost("upload")]
    [RequestSizeLimit(200 * 1024 * 1024)]   // 200 MB, this action only
    public async Task<IResult> Upload(IFormFile file)
    {
        await using var stream = System.IO.File.Create(
            Path.Combine("uploads", file.FileName));
        await file.CopyToAsync(stream);
        return Results.Ok(new { file.FileName, file.Length });
    }
}

Swap [RequestSizeLimit(...)] for [DisableRequestSizeLimit] when the action must accept arbitrarily large bodies and you have another guard (streaming, a quota check) in place.

3. Set the limit per request in minimal APIs

Minimal APIs do not honor [RequestSizeLimit] the way controllers do, because there is no MVC resource filter in the pipeline to read it. Set the limit imperatively through IHttpMaxRequestBodySizeFeature, before anything reads the body:

// .NET 11, ASP.NET Core 11, C# 14
app.MapPost("/upload", async (HttpContext context) =>
{
    var sizeFeature = context.Features.Get<IHttpMaxRequestBodySizeFeature>();
    if (sizeFeature is not null && !sizeFeature.IsReadOnly)
    {
        sizeFeature.MaxRequestBodySize = 200 * 1024 * 1024;   // 200 MB
    }

    using var reader = new StreamReader(context.Request.Body);
    var content = await reader.ReadToEndAsync();
    return Results.Ok(new { bytes = content.Length });
});

Check IsReadOnly first. Once the server has started reading the body, the limit is locked and assigning to it throws InvalidOperationException. That is also why this technique does not work with an IFormFile parameter on a minimal API: model binding reads the multipart body before your handler runs, so by the time you can touch the feature it is already read-only (this is the root of dotnet/aspnetcore issue #50871, where large multipart uploads to a minimal API silently returned 200 with a truncated file). For minimal API uploads, bind HttpContext or HttpRequest, raise the limit, then read the form yourself with request.ReadFormAsync(), or set a group-wide limit with a small middleware on a MapGroup.

4. Raise the multipart form limit for form posts

If your 413 is actually a 400 with an InvalidDataException about a multipart body length, the limit that fired is FormOptions.MultipartBodyLengthLimit, not Kestrel’s. Raise it globally through options, or per controller action with [RequestFormLimits]:

// .NET 11, ASP.NET Core 11, C# 14 -- global, in Program.cs
builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = 200 * 1024 * 1024;   // 200 MB
});
// .NET 11, ASP.NET Core 11, C# 14 -- per action
[HttpPost("upload")]
[RequestSizeLimit(200 * 1024 * 1024)]
[RequestFormLimits(MultipartBodyLengthLimit = 200 * 1024 * 1024)]
public async Task<IResult> Upload(IFormFile file) { /* ... */ }

For a large upload you typically need both raised: MaxRequestBodySize governs the whole request, and MultipartBodyLengthLimit governs the form body inside it. Set them to the same value.

5. Behind IIS, raise maxAllowedContentLength in web.config

When you host behind IIS (in-process or out-of-process), the IIS request-filtering module enforces maxAllowedContentLength, default 30,000,000 bytes, before the request reaches Kestrel. Raising the Kestrel limit does nothing until you also raise this. Add it to web.config:

<!-- web.config -- value is in bytes; 200 MB here -->
<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="209715200" />
    </requestFiltering>
  </security>
</system.webServer>

The in-process hosting module has a second, separate limit you can set in code through IISServerOptions:

// .NET 11, ASP.NET Core 11, C# 14 -- in-process IIS hosting
builder.Services.Configure<IISServerOptions>(options =>
{
    options.MaxRequestBodySize = 200 * 1024 * 1024;   // 200 MB
});

Behind IIS you may need all three aligned: maxAllowedContentLength in web.config, IISServerOptions.MaxRequestBodySize, and Kestrel’s MaxRequestBodySize. The request is rejected by whichever is smallest.

6. Raise the cap on your reverse proxy

If a proxy sits in front of the app, it has its own limit and returns 413 before your app is involved. On nginx, the directive is client_max_body_size (default 1 MiB, which is far smaller than Kestrel’s default and a common surprise):

# nginx.conf -- allow 200 MB uploads through the proxy
client_max_body_size 200M;

For YARP, an API gateway, or a cloud load balancer, find the equivalent request-size setting in that layer’s configuration. Raising the app-side limits will not help until the proxy lets the body through.

Gotchas and variants

The mental model: an upload passes through a stack of size gates (proxy, IIS, Kestrel body size, multipart form size), and it is rejected by the first one it exceeds. Find the layer that produced your 413, raise that limit to a real, bounded value, and pair it with streaming so a bigger limit does not become a bigger liability.

Sources

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Back