IIS / ASP.NET : corriger l’erreur 0x80070008 « Not enough memory resources » sur Windows Server 2019 (pool non paginé, 32 bits, quotas IIS)

Votre application ASP.NET sous IIS affiche « Not enough memory resources are available to process this command (0x80070008) » sur Windows Server 2019, alors que le serveur a 64 Go de RAM ? Suivez ce guide opérationnel pour trouver la vraie limite atteinte et corriger durablement.

Sommaire

Vue d’ensemble de la question

Sur un serveur Windows Server 2019 doté de 64 Go de RAM, une application IIS/ASP.NET se termine brutalement avec :

System.Runtime.InteropServices.COMException (0x80070008)
Not enough memory resources are available to process this command

Ce message ne signifie pas que la « RAM physique » est totalement consommée. Il indique plutôt que Windows ne peut plus allouer une ressource mémoire particulière : souvent le pool non paginé du noyau, parfois le pool paginé, l’espace d’adressage 32 bits d’un processus, la desktop heap, ou qu’un quota IIS a été atteint. L’objectif est de mesurer la contrainte réelle, d’identifier la ou les causes (fuite de handles, mauvais paramétrage, pilotes/EDR) et d’appliquer un correctif stable.

Réponse & solutions (vue synthétique)

ÉtapeActionPourquoi / ce qu’elle vérifie ou corrige
1. Réparer les fichiers systèmeExécuter, en invite Administrateur :
DISM /Online /Cleanup-Image /ScanHealth
DISM /Online /Cleanup-Image /RestoreHealth
sfc /scannow
puis redémarrer
Écarte les dysfonctionnements liés à des fichiers corrompus (.dll, pilotes, WinSxS)
2. Mettre à jour• Appliquer le dernier cumulative update Windows Server 2019
• Mettre à jour .NET / le framework de l’application
De nombreux correctifs mémoire arrivent via Windows Update et les patches .NET
3. Vérifier le fichier d’échangeTaille minimale ≈ taille RAM, jamais désactivéSans pagefile, Windows ne peut pas évacuer certains pools / augmenter le commit limit
4. Chercher la vraie limite atteinteSurveiller avec Performance Monitor :
Memory\Available MBytes
Memory\Pool Nonpaged Bytes
Memory\Pool Paged Bytes
Process(w3wp)\Handle Count / Private Bytes
L’erreur survient souvent quand le pool non paginé ou une plage d’adressage 32 bits est saturée, même si la RAM est libre
5. Contrôler la configuration IIS• Désactiver « Enable 32‑bit Applications » (préférer 64‑bits)
• Adapter/retirer les limites « Private Memory Limit (KB) » et « Virtual Memory Limit (KB) » des Application Pools
Un processus 32 bits est borné (~2 Go utilisables) ; un quota IIS trop bas force un recyclage abrupt
6. Détecter les fuites de handles / GDIhandle.exe -u w3wp.exe (Sysinternals)
• PerfMon : Process\Handle Count, Process\GDI Objects
Les fuites d’objets noyau épuisent le pool non paginé
7. Inspecter les dumps si le défaut persisteprocdump -ma w3wp.exe ⇒ analyse WinDbg : !vm, !poolused, !analyze -vPour identifier la classe d’objets ou le module responsable
8. Ajuster la gestion mémoire du noyau (facultatif)Registre :
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management
PoolUsageMaximum (ex. 70)
PagedPoolSize
(Changer uniquement après analyse, puis redémarrer)
Relève légèrement les plafonds du pool — pas un correctif durable en cas de fuite
9. Bureau à distance / Desktop HeapPour des applis à forte création de fenêtres (GDI), ajuster la desktop heap dans :
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems\Windows
Son dépassement peut produire également 0x80070008

Mode opératoire détaillé

1) Réparer et assainir le socle Windows

  1. Ouvrez Invite de commandes (Admin) puis exécutez : DISM /Online /Cleanup-Image /ScanHealth DISM /Online /Cleanup-Image /RestoreHealth sfc /scannow Redémarrez. Cela répare les composants WinSxS et des DLL susceptibles de provoquer des comportements erratiques dans IIS, HTTP.SYS ou le noyau mémoire.
  2. Installez les mises à jour Windows Server 2019 et .NET/.NET Framework pertinentes (y compris les cumulative updates). Beaucoup de correctifs de gestion mémoire, de GDI et de pools y sont intégrés.

2) Pagefile : garant du commit système

Le « commit limit » (capacité totale d’allocation virtuelle confirmée) ≠ la RAM physique. Il dépend fortement de la présence et la taille du fichier d’échange (pagefile). Sans pagefile, certains pools ne peuvent jamais s’étendre et vous verrez 0x80070008 malgré des gigaoctets libres.

  • Recommandation : pagefile activé, taille minimale ≈ taille de la RAM (ex. 64 Go) ou gestion automatique par Windows.
  • Vérifiez rapidement : wmic pagefile list /format:list ou en PowerShell : Get-CimInstance Win32_PageFileSetting | Format-List *

3) Identifier la vraie ressource épuisée

Créez un jeu de collecteurs de données (PerfMon) sur 10–30 min couvrant les compteurs ci‑dessous. L’objectif : distinguer pool non paginé, pool paginé, commit global, GDI/USER, et limites par processus.

Objet\CompteurIndicateur d’alerteInterprétation
Memory\Available MBytes< 1000 MB de façon durablePression mémoire globale, mais pas forcément la cause du 0x80070008
Memory\Pool Nonpaged BytesMonte régulièrement puis plafonne (près de la limite de build)Saturation du pool non paginé (fuite de handles/objets noyau, pilote, EDR)
Memory\Pool Paged BytesTrès élevé et croissantPression sur le pool paginé (drivers, composants noyau)
Process(w3wp)\Private BytesSe rapproche du quota IIS ou d’une limite 32 bitsQuota atteint : recyclage ou crash à prévoir
Process(w3wp)\Handle CountMonter en escalier sans redescendreFuite de handles (Section, File, RegKey, Event, etc.)
Process(w3wp)\GDI Objects / USER Objects> 7 500 et continue de croîtreFuite GDI/USER ; risque de desktop heap & 0x80070008
.NET CLR Memory\# Bytes in all Heaps (process w3wp/dotnet)Augmente sans GC significatifFuite managée (objets conservés), différente d’une fuite noyau

Collecte express via PowerShell :

$counters = '\Memory\Available MBytes',
            '\Memory\Pool Nonpaged Bytes',
            '\Memory\Pool Paged Bytes',
            '\Process(w3wp)\Private Bytes',
            '\Process(w3wp)\Handle Count'
Get-Counter -Counter $counters -SampleInterval 5 -MaxSamples 60 | Export-Counter -Path C:\Temp\mem.blg -FileFormat blg

4) Vérifier & corriger la configuration IIS

  • Forcer 64 bits : dans Application Pools → Advanced Settings, mettez Enable 32-bit Applications à False pour les pools qui exécutent l’application. En ligne de commande : %windir%\system32\inetsrv\appcmd set apppool /apppool.name:"MonPool" /enable32BitAppOnWin64:false Pourquoi ? Un w3wp 32 bits se heurte vite à l’espace d’adressage (~2 Go utilisables par défaut), indépendamment de la RAM.
  • Levée des quotas mémoire si injustifiés : %windir%\system32\inetsrv\appcmd set apppool "MonPool" /recycling.periodicRestart.privateMemory:0 %windir%\system32\inetsrv\appcmd set apppool "MonPool" /recycling.periodicRestart.memory:0 Des valeurs trop basses (privateMemory / memory) déclenchent un recyclage agressif ou un échec d’allocation assimilé à 0x80070008.
  • ASP.NET Core hébergé sous IIS : le travail réel est dans dotnet.exe derrière w3wp (ANCM). Surveillez les deux processus (Process(dotnet)\Private Bytes, Handle Count) et appliquez la politique 64 bits côté build et pool.
  • Recyclage contrôlé : planifiez un recyclage proactif en heures creuses si une fuite lente est suspectée, le temps de corriger. Évitez les recyclages trop fréquents qui masquent le problème.

5) Détecter une fuite de handles / GDI

Utilisez les utilitaires Sysinternals (pas de lien nécessaire si déjà disponibles sur votre bastion d’admin) :

  • handle.exe -u w3wp.exe pour lister et classer les handles ouverts par type et par module. Recherchez des dizaines de milliers de Section, File, RegKey ou Token qui montent sans redescendre.
  • Process Explorer : inspectez Handle Count, GDI Objects, USER Objects. Au‑delà de ~7 500–9 000 GDI, vous êtes proche de la limite par processus (10 000 par défaut) ; dépassement = erreurs d’allocation + crashs.

Indices applicatifs côté .NET (à corriger par code) :

  • Objets non‑IDisposable non libérés ? Bitmap, Graphics, SqlConnection, HttpWebResponse, FileStream, etc. Assurez‑vous qu’ils sont using ou Dispose().
  • Fuites GDI classiques (génération d’images à la volée) : ne stockez pas des Bitmap dans le cache indéfiniment. Préférez des caches à TTL et supprimez explicitement les objets.
  • Cache managé non borné (MemoryCache, IMemoryCache) : fixez une size limit + politiques d’expiration, faute de quoi le LOH et la pression GC dérivent.

Exemples C# :

// OK : libère immédiatement les ressources natives
using (var bmp = new Bitmap(w, h))
using (var g = Graphics.FromImage(bmp))
{
    // ...
} // Dispose() garanti

// OK : réponse HTTP libérée
using var resp = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
using var stream = await resp.Content.ReadAsStreamAsync();

// OK : cache borné
services.AddMemoryCache(o => o.SizeLimit = 256);
cache.Set(key, value, new MemoryCacheEntryOptions { Size = 1, SlidingExpiration = TimeSpan.FromMinutes(10) }); 

6) Analyser un dump quand l’erreur survient

Déclenchez un full user dump du processus fautif au premier signal d’exception :

mkdir C:\Dumps
procdump -ma -e 1 -w w3wp.exe C:\Dumps

Puis ouvrez le dump dans WinDbg :

!analyze -v
!vm
!poolused 2
!handle 0 7
  • !vm : vue d’ensemble du commit, du pool non paginé/paginé et des limites.
  • !poolused 2 : consommation du pool par tag. Un tag explose ? Ciblez‑le avec PoolMon ou la télémétrie ETW pour trouver le module.
  • !handle : type de handles retenus (fuite) et pile d’allocation si disponible.

Astuce : si l’erreur est sporadique, installez un dump automatique via le registre (LocalDumps) sur w3wp/dotnet pour capturer l’instant critique.

7) Ajustements du noyau (avancés, à manier avec parcimonie)

Seulement après diagnostic concluant, et si une correction applicative tarde, vous pouvez desserrer légèrement certains plafonds :

HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management
  PoolUsageMaximum = 70 (DWORD)
  PagedPoolSize    = 0xFFFFFFFF (auto) ou une valeur adaptée

Redémarrage requis. Ces réglages donnent un peu d’air mais ne corrigent pas une fuite active.

8) Desktop Heap / sessions RDP

Des serveurs qui ouvrent de multiples sessions RDP ou créent dynamiquement de nombreuses fenêtres (reporting, génération d’images, automations) peuvent saturer la desktop heap, entraînant 0x80070008 et des erreurs « Cannot create window ».

Paramètre (avancé) :

HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems\Windows
  ... SharedSection=1024,20480,768

Augmenter prudemment le troisième nombre (non‑interactive desktop) après test. Documentez et surveillez GDI/USER par processus.

9) Antimalware/EDR et pilotes

  • Excluez de l’analyse en temps réel C:\Windows\WinSxS, %TEMP% de .NET, et les dossiers d’assemblage temporaires. Le verrouillage agressif peut perturber le chargement d’images et accroître la consommation de handles.
  • Vérifiez les pilotes récemment ajoutés (stockage, réseau, sauvegarde/instantanés). Un pilote consommant le pool non paginé apparaîtra via !poolused / PoolMon.

Plans d’action prêts à l’emploi

Check rapide (15 minutes)

  1. Confirmez pagefile activé & dimensionné.
  2. Vérifiez que le pool IIS est 64 bits et que ses quotas mémoire ne sont pas restrictifs.
  3. Lancez PerfMon et surveillez Pool Nonpaged Bytes, Handle Count, Private Bytes.
  4. Si Handle Count grimpe en continu : capturez handle.exe -u w3wp.exe et planifiez un dump.

Stabilisation (même journée)

  • Appliquez les mises à jour OS et .NET.
  • Recyclage préventif des pools aux heures creuses.
  • Ajoutez une alerte PerfMon : si Pool Nonpaged Bytes > 80 % de la limite pendant 2 min → déclencher un script de collecte (dump + handle) et redémarrage contrôlé du pool.

Remédiation durable (S+1)

  • Corrigez le code : dispose systématique, caches bornés, pas d’images GDI maintenues, pas d’accès fichier/stream laissés ouverts.
  • Nettoyez/replacez les pilotes à l’origine d’un tag de pool problématique.
  • Normalisez les exclusions EDR et formalisez un Data Collector Set permanent.

Diagnostic différentiel : comment lire vos mesures

SymptômesCause la plus probableCorrectif prioritaire
Pool Nonpaged Bytes grimpe jusqu’à une valeur plafond, handles en hausse, RAM encore disponibleFuite d’objets noyau (handles, GDI/USER), pilote ou EDRIdentifier le type de handle avec handle.exe, tag de pool avec !poolused / PoolMon, mettre à jour/retirer le composant fautif
Private Bytes du processus 32 bits > 1,5–1,8 GoEspace d’adressage 32 bits saturéBasculer l’app pool et le build en 64 bits, supprimer les quotas, revoir les caches
Available MBytes < 500 MB, swapping intensePression mémoire globaleAugmenter le pagefile, réduire les charges, identifier le ou les processus gloutons
GDI/USER > 9 000 objets, erreurs de création de fenêtreFuite GDI/desktop heapCorriger le code créant des fenêtres/bitmaps, ajuster la desktop heap si nécessaire
Crashs synchrones aux recyclages planifiésQuotas IIS trop bas, recycle « brutal »Mettre privateMemory/memory à 0 (illimité) ou à une valeur réaliste, lisser les recyclages

Bonnes pratiques côté code ASP.NET (prévenir plutôt que guérir)

  • Dispose partout : base de données, streams, images, réponses HTTP, Timer, CancellationTokenSource
  • Cache borné : MemoryCache/IMemoryCache avec taille max et TTL. Évitez les dictionnaires statiques qui grandissent sans borne.
  • Pas de GDI sur le chemin chaud : si possible, externalisez la génération d’images vers un service dédié, ou vérifiez strictement la libération des Graphics/Bitmap.
  • Surveillance applicative : exposez un endpoint interne « health » qui remonte handle count, private bytes, GDI/USER pour alerter avant saturation.
  • Server GC (ASP.NET Framework et Core) : activé par défaut sur serveur ; ce réglage optimise le throughput, mais ne corrige pas une fuite noyau. Ne pas confondre OOM managé et 0x80070008 d’origine noyau.

Commandes utiles (mémo)

:: Etat du commit & pools
rammap.exe           :: vue avancée (si disponible)
poolmon.exe /n      :: tags de pool (activer le "pool tagging" si requis)

:: Perf rapide
typeperf "\Memory\Pool Nonpaged Bytes" -si 5 -sc 60

:: IIS (64 bits + quotas)
%windir%\system32\inetsrv\appcmd list apppool
%windir%\system32\inetsrv\appcmd set apppool /apppool.name:"MonPool" /enable32BitAppOnWin64:false
%windir%\system32\inetsrv\appcmd set apppool "MonPool" /recycling.periodicRestart.privateMemory:0
%windir%\system32\inetsrv\appcmd set apppool "MonPool" /recycling.periodicRestart.memory:0

:: Dumps
procdump -ma -e 1 -w w3wp.exe C:\Dumps

:: Handles
handle.exe -u w3wp.exe > C:\Temp\handles.txt 

Environnements virtualisés et conteneurs : points d’attention

  • VM avec mémoire dynamique : un ballooning agressif peut réduire le commit limit pratique. Assurez‑vous d’une réserve minimale suffisante et d’un pagefile présent dans la VM, pas seulement sur l’hôte.
  • ASP.NET Core en conteneur Windows : vérifiez les job objects/quotas mémoire côté host. Des limites trop serrées se traduisent par des échecs d’allocation analogues à 0x80070008.

Informations complémentaires utiles

  • Origine de 0x80070008 : erreur COM quand Windows ne peut plus allouer des ressources noyau ; elle concerne le pool ou l’espace d’adressage, pas la RAM totale.
  • Redémarrage : libère temporairement le pool mais masque la cause. Il vaut mieux identifier la fuite (module tiers, pilote, librairie GDI, objets IDisposable non libérés, etc.).
  • Surveillance continue : un Data Collector Set PerfMon ou Windows Admin Center peut déclencher une alerte avant saturation et recycler en douceur.
  • Antivirus / EDR : exclure C:\Windows\WinSxS et les dossiers temporaires .NET pour éviter des blocages.
  • 64 bits partout : compiler en AnyCPU (préférer 64 bits) et exécuter le pool IIS en 64 bits pour éviter la fenêtre de 2 Go d’adresse.

FAQ rapide

Q : Pourquoi j’ai 64 Go de RAM libres et pourtant 0x80070008 ?
R : Parce qu’une ressource spécifique (pool non paginé/paginé, handles, desktop heap, espace d’adressage 32 bits) est épuisée. La RAM libre n’aide pas si le noyau refuse un type d’allocation précis.

Q : Comment savoir si c’est le pool non paginé ?
R : Surveillez Memory\Pool Nonpaged Bytes ; s’il ne cesse de croître jusqu’à un plafond puis que l’application échoue, c’est typique. Les dumps (!poolused) confirmeront le tag responsable.

Q : Relever PagedPoolSize/PoolUsageMaximum suffit‑il ?
R : Non. C’est un palliatif. La vraie correction consiste à réparer la fuite (code, module, pilote) ou à supprimer la contrainte (32 bits/quota).

Q : Dois‑je augmenter le « Private Memory Limit (KB) » IIS ?
R : Si la valeur est trop basse, oui ; sinon laissez à 0 (illimité) et surveillez. Un quota trop strict provoque recyclages et erreurs d’allocation.

Conclusion

Le message « Not enough memory resources are available to process this command (0x80070008) » n’est que le symptôme d’une vraie contrainte : pool noyau, handles, GDI, desktop heap, quotas IIS ou espace 32 bits. En appliquant d’abord DISM/SFC et les mises à jour, puis une surveillance ciblée (Nonpaged Bytes, Handle Count, Private Bytes), vous isolez rapidement la cause. Les correctifs durables passent par le tout 64 bits, la suppression des quotas inadaptés, la réparation du code (dispose, caches bornés) et, si nécessaire, l’ajustement mesuré de la configuration noyau. Résultat : stabilité retrouvée, recyclages prédictibles, et fin des crashs mystères malgré des dizaines de gigaoctets de RAM inutilisés.

Sommaire