36

I'm testing ASP Net Core 2.2 using default project made from:

Visual Studio > File > New > Project > ASP NET Core Web Application > Next > Create. Press IIS Express button on interface and automatically go to https://localhost:44356/api/values

Edit: The point is, I'm testing ASP Net Core in case of brute force spamming from people.

I have only modified this:

        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value2" };
        }

Edit: to this, I edited my code to reasonable test, producing minimal reproducable example

// GET api/values

[HttpGet]
public ActionResult<IEnumerable<Post>> Get()
{
    // Testing for brute force
    return Enumerable.Range(0, 100).Select(x => "Example Value").ToList();
}

The starting Process Memory is 80 MB.

However, when I tried refreshing the page, the starting memory keeps coming up: edited test

Edit: Turns out this issue has been there from 2 years ago, with no solution, and with Real Life Example Experience, https://github.com/aspnet/AspNetCore/issues/1976

I'm pretty sure that yellow arrow means GC is working correctly, but my process memory usage isn't coming down every request. I'm not sure if this is a feature or bug from ASP Net Core, I expected it to go down after request / did not increase everytime a user GET the /values

Edit: I don't want people dangerously spamming my app and it starting to crash down because memory leak. And I'm afraid to use Net Core for my company if this solution is not fixed yet.

Why ASP Net Core 2.2 do not release memory?

I have tried testing in two different laptop, the result is same.

Edit: My colleague also have this problem.

Edit: I have tried using dot net core v2.1, the result is same.

I have tried GCCollect(); the result is same.

old image ref.

Starting Process Memory

Process Memory keeps coming up

Kevin Tanudjaja
  • 866
  • 1
  • 9
  • 16
  • 1
    Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/198268/discussion-on-question-by-kevin-tanudjaja-why-asp-net-core-2-2-do-not-release-me). – Samuel Liew Aug 22 '19 at 00:19
  • 1
    By design? GC collected runtimes generally hold on to memory unless it’s running low – Dave Hillier Mar 22 '20 at 09:23
  • We are also facing the problem in the latest dotnet projects, all implementations from net5, net6 and net7 are similar. After optimization efforts in the project source code itself but not feasible. We finally found the answer here: [Runtime configuration options for garbage collection](https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector). After "requesting" the freer by `workstation` mechanism, instead of `server` as default. Our services worked without using too much memory, the load tests used only ~1/15 of the ram than before. – Henry Trần Dec 29 '22 at 11:30

4 Answers4

48

ASP.NET Core can appear to use more memory than it should because it's configured to use Server GC by default (as opposed to Workstation GC). For a discussion of a similar concern see here. In theory, the application should be able to reduce its memory footprint when your server faces a memory pressure.

More on this topic in this MSDN doc and Github CoreCLR doc.

To see if it's indeed Server GC that causes the additional memory consumption, you can set GC strategy to workstation in your csproj file:

<PropertyGroup> 
    <ServerGarbageCollection>false</ServerGarbageCollection>
</PropertyGroup>
scharnyw
  • 2,347
  • 2
  • 18
  • 32
14

!!!LAST EDIT: THIS SOLUTION IS NOT THE BEST WAY, SOME MIGHT SAY THIS IS A TERRIBLE SOLUTION... I RECOMMEND THE ANSWER BELOW!!!

I was struggling with the same issue. After a little research i found that .NET Core allocating new memory space if it can. It will be released if it is necessary, but this release is more resource demanding than the other releasing, because you have to use the GC this way:

GC.Collect(2, GCCollectionMode.Forced, true);
GC.WaitForPendingFinalizers();

My solution was to create a middleware:

using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace RegisterValidator
{
    public class GCMiddleware
    {
        private readonly RequestDelegate _next;

        public GCMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext httpContext)
        {
            await _next(httpContext);
            GC.Collect(2, GCCollectionMode.Forced, true);
            GC.WaitForPendingFinalizers();
        }
    }
}

EDIT: this middleware will provide a GC.Collect after every call.

And register it in the Configure() method in Startup.cs:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider service, ILoggerFactory loggerFactory)
    {

        app.UseMiddleware<GCMiddleware>();
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }

        app.UseStaticFiles();
        //app.UseHttpsRedirection();
        app.UseMvcWithDefaultRoute();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync("\"No ");

        });
    }

Maybe there is another solution but I haven't found it yet. I hope it helps.

Edit: If you aren't sure that the memory usage is because there is a huge amount of allodated space of some other issue you can use dotMemory that will profile your memory usage with more detail than your Visual Studio.

turanszkik
  • 494
  • 5
  • 15
  • Edit: Nevermind, after some more testing using your Middleware, the process memory have lower increase rate than without your Middleware, It's great for my solution for now, though I'm unaware of the side effects calling GC. – Kevin Tanudjaja Aug 22 '19 at 04:15
  • 1
    @Kevin I'm happy that it helped. In theory this kind of memory allocation is increasing the performance because the program will no need to ask for memory space from the OS in case of high memory usage. The GC's secound generation cleaning that I have used in my middleware has more computing needs than the lower generations. That's why the .NET doesn't do it every time you see a CG activity in your memory monitoring window in VS. Maybe the best solution would be to measure somehow the difference in the performance with and without the middleware. – turanszkik Aug 22 '19 at 06:42
  • This is really great. I had really bad problems on my linux server where I was hosting my own microservices architecture and it was causing me headaches to figure out, why it's consuming so much memory. Thanks to your solution the apps are now more stable and are using only necessary memory. Also we're migrating to .NET Core 3.1 and I would like to test it with and without this GC Middleware. – Tomas Blanárik Mar 13 '20 at 08:22
  • 21
    This is a terrible solution. Dont use it. – DarthVader Apr 24 '20 at 06:58
  • @DarthVader Can you please explain me, why this is a terrible solution, because we are planning to use this in our project. – Purnima Naik Apr 24 '20 at 16:02
  • 1
    @PurnimaNaik because in a real app, you should never call GC.Collect() explicitly. You can read it all over the internet. That is a big sin. Same applies in Java. What is the point of having a managed memory, if you will invoke collect in every request. – DarthVader Apr 26 '20 at 12:19
  • 2
    Forcing a collect on each request is terrible for performance – Kristoffer Schroeder Oct 09 '20 at 20:21
2

TL;DR; Just make sure your DBContext is not Transient.

I tried both the answers but in my case issue was entirely different. At the start of the project one of the devs, for some experiment, made the application context's lifetime to be transient. The default life time of the context variable is scoped and which is what it should be, at least in our case.

services.AddDbContext<AppContext>(ServiceLifetime.Transient);

To keep context as scoped you can inject it following two ways:

  • services.AddDbContext<AppContext>();
  • services.AddDbContext<AppContext>(ServiceLifetime.Scoped);

Brief Explanation: Just like any other service in API, DBContext can also be injected with lifetime as

  1. Transient - Creates new instance of DBContext for each repository call
  2. Scoped - Creates one instance of DBContext for one API call
  3. Singleton - Creates once instance of DBContext for all API calls

So if you make DBContext Transient and your one API call is getting data from 10 different repos. This will create 10 different instance of DBContext. The memory consumption will be very quick and GC will not have enough time to release the memory. This can crash the site in less than an hour with just 2-3 users.

manjeet lama
  • 185
  • 3
  • 12
0

I found the solution worked for me

Edit the runtimeconfig.json file, and set this values:

{
   "runtimeOptions": {
      "configProperties": {
         "System.GC.HeapHardLimitPercent": 1
      }
   }
}

The lower you set the HeapHardLimitPercent value, the more aggressively the GC will work.

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77