Org Skills

dotnet-performance

Diagnose and fix .NET and ASP.NET Core performance problems — measure first with BenchmarkDotNet, dotnet-counters, dotnet-trace, dotnet-dump and APM/OpenTelemetry, then reduce allocations, fix thread pool starvation and sync-over-async, tune EF Core queries, add caching and output caching, choose GC modes, and weigh Native AOT and ReadyToRun.

Download .zip Raw Source
When agents use itUse this whenever the user says a .NET API is slow, has high latency, high CPU, growing memory, OutOfMemoryException, GC pauses, timeouts under load, slow startup, thread pool starvation, slow EF Core queries or N+1, or asks about Span<T>, ArrayPool, ValueTask, benchmarks, *.csproj settings like ServerGarbageCollector or PublishAot, Kestrel or HttpClient tuning. Also apply it when reviewing C# hot paths, DbContext queries or LINQ-heavy code for performance, even if the user did not mention speed.

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill dotnet-performance -a github-copilot
# or with the org installer (adds .github/skills/dotnet-performance):
npx -y github:AGCO-Global/org-skills add skill dotnet-performance

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill dotnet-performance
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill dotnet-performance -g

Installs the backend-dotnet-skills plugin, which bundles all Backend / .NET skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install backend-dotnet-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill dotnet-performance -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill dotnet-performance -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill dotnet-performance -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill dotnet-performance -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Skill contents

.NET Performance

Measure first, change one thing, measure again. Most real-world .NET slowness is I/O (queries, chatty HTTP, missing caching) or blocking, not CPU micro-costs — never recommend Span<T> rewrites before a profile shows the hot path.

1. Measure before touching code

Question Tool Capture
Is this method faster/leaner? BenchmarkDotNet ([MemoryDiagnoser], Release build, no debugger) Mean, allocations/op, compare baselines
What is the live process doing? dotnet-counters (dotnet-counters monitor -p <pid>) CPU, GC heap size, gen0/1/2 counts, % time in GC, thread pool queue length and thread count, exception rate
Where is CPU/wall time going? dotnet-trace (open in PerfView, Visual Studio or speedscope) Hot stacks, blocked time
Memory leak / high memory dotnet-gcdump, dotnet-dump (analyze, dumpheap -stat, gcroot) Top types by size, roots holding them
Windows deep dive PerfView GC events, allocation stacks, ETW
Production, per request APM / OpenTelemetry traces and metrics p95/p99 latency, dependency spans, slow SQL
Load k6, NBomber, Bombardier, JMeter Throughput and latency percentiles at a target RPS

Always get: the endpoint/operation, current p95/p99 and target, and whether it's CPU, memory, I/O or waiting.

2. Triage table

Symptom Likely cause Check Fix
Latency climbs under load, CPU low, thread count climbing Thread pool starvation from sync-over-async Thread pool queue length > 0 in counters; stacks in .Result/.Wait()/Task.Run wrappers Async all the way; remove blocking calls; don't raise SetMinThreads as the fix
Endpoint slow, DB time dominant N+1, missing index, over-fetching EF logged SQL / trace spans; query plan Projection, Include/split query, index, pagination
High % time in GC, gen2 growing Allocation-heavy hot path, LOH allocations (≥ 85 KB) dotnet-counters GC stats, allocation profile Pooling, Span<T>, streaming, avoid large buffers
Memory grows until restart Leak: static caches, event handlers, unbounded IMemoryCache, captured scopes gcdump diff between two snapshots, gcroot Bounded cache with SizeLimit, unsubscribe, fix lifetimes
Socket exhaustion / DNS stale new HttpClient() per call or static client forever netstat TIME_WAIT counts IHttpClientFactory, PooledConnectionLifetime
Slow cold start JIT, reflection-heavy startup, large DI graph Startup trace ReadyToRun, trim startup work, Native AOT if compatible
High CPU, steady Hot loop, regex, serialization, logging CPU trace hot stacks Source-generated regex/JSON/logging, algorithm fix
Timeouts to downstream No timeout, retry storms Resilience telemetry Timeouts, circuit breaker, jittered retries

3. Allocation reduction (only on proven hot paths)

  • Span<T>/ReadOnlySpan<char> and stackalloc (small, bounded sizes) for parsing and slicing instead of Substring/Split.
  • ArrayPool<T>.Shared.Rent/Return (in try/finally) for temporary buffers; RecyclableMemoryStream for pooled streams.
  • Avoid LINQ in tight loops (delegate + enumerator allocations); use for/foreach over arrays or List<T>. LINQ is fine outside hot paths.
  • Strings: StringBuilder or interpolated string handlers for building; string.Create for fixed layouts; StringComparison.Ordinal(IgnoreCase) instead of ToLower() comparisons.
  • Avoid boxing: generic constraints instead of object, no value types through non-generic interfaces, struct enumerators.
  • Lookups: FrozenDictionary/FrozenSet (.NET 8+) for read-mostly data; SearchValues<T> for character/byte set searches.
  • Source generators: [LoggerMessage], [GeneratedRegex], System.Text.Json JsonSerializerContext.
  • ValueTask only where the result is usually synchronous and measured to matter.
[Benchmark(Baseline = true)] public int Split() => Input.Split(',').Length;
[Benchmark] public int SpanCount() => Input.AsSpan().Count(',') + 1;

4. Async pitfalls

  • Sync-over-async (.Result, .Wait(), GetAwaiter().GetResult()) is the number-one cause of throughput collapse; a blocked request holds a thread while waiting for another thread.
  • Task.Run in ASP.NET Core request code only adds a thread hop — don't wrap sync work to "make it async".
  • Parallelise independent I/O with Task.WhenAll; bound concurrency with Parallel.ForEachAsync (MaxDegreeOfParallelism) or SemaphoreSlim — never unbounded fan-out against a database.
  • Synchronous I/O (Stream.Read, sync EF calls) on request threads is disallowed by default in Kestrel for a reason; don't re-enable it.

5. EF Core performance

Technique When
AsNoTracking() + Select projection to DTO Every read-only query — biggest single win
Pagination (keyset for deep pages) Any list endpoint; never return unbounded sets
AsSplitQuery() Multiple collection Includes causing cartesian explosion
ExecuteUpdateAsync/ExecuteDeleteAsync Set-based updates/deletes (EF Core 7+)
Batching (default in SaveChanges) Many inserts; tune MaxBatchSize only if measured
EF.CompileAsyncQuery Very hot, identical queries where translation cost shows in profiles
AddDbContextPool High-throughput services with cheap contexts
Indexes Match WHERE/ORDER BY/join columns; verify with the actual execution plan

Enable LogTo/sensitive-free SQL logging in development and count queries per request; more than a handful for one endpoint usually means N+1. Drop to Dapper or raw SQL (SqlQuery<T>) for reporting queries EF translates poorly.

6. Caching

  • IMemoryCache for per-instance data; always set expiration and a SizeLimit with entry sizes.
  • IDistributedCache (Redis etc.) when instances must share. HybridCache (Microsoft.Extensions.Caching.Hybrid, introduced alongside .NET 9) combines both tiers with stampede protection — check the package version available to the project.
  • Output caching (AddOutputCache, CacheOutput()) for whole responses with tag-based eviction; response caching middleware only for HTTP-cache-header semantics.
  • Response compression: prefer doing it at the reverse proxy/CDN; if in-app, be aware of BREACH-style risks for HTTPS responses mixing secrets and user input.
  • Define invalidation before adding a cache; cache stampede and stale data are bugs too.

7. Runtime and hosting settings

Setting Default guidance
GC mode Server GC for ASP.NET Core (the default); Workstation for small containers/sidecars/desktop
DATAS (dynamic adaptation to app size) Enabled by default with Server GC from .NET 9; lowers memory in containers — measure throughput if you disable it
Container limits Set CPU/memory limits; the runtime respects them. Consider GCHeapHardLimit/GCHeapHardLimitPercent only after measuring
Tiered compilation / Dynamic PGO Leave on (default since .NET 8)
ReadyToRun Faster startup, larger binaries; good for scale-to-zero and CLI tools
Native AOT Fastest startup, smallest memory; requires trimming-safe code, source-generated JSON, minimal APIs (no MVC), limited reflection — verify every dependency's AOT support
Kestrel Defaults are sane; set Limits (body size, connections) for protection, not speed
HttpClient IHttpClientFactory; SocketsHttpHandler.PooledConnectionLifetime; MaxConnectionsPerServer only if downstream demands it; HTTP/2 multiplexing for gRPC

Deliverable format

Report performance work as:

  1. Baseline — metric, tool and conditions (build, load, data size).
  2. Diagnosis — evidence (counter values, trace stacks, SQL) pointing to the cause.
  3. Fix — the smallest change, as a diff, with the trade-off stated.
  4. Result — the same measurement after the change, and a regression guard (benchmark, load test threshold or alert).

Anti-patterns to reject

  • Optimising without a profile or benchmark; benchmarks in Debug or with a stopwatch loop.
  • Raising ThreadPool.SetMinThreads to mask sync-over-async.
  • GC.Collect() calls in application code.
  • Tracking queries, ToList() before filtering, unbounded result sets, lazy loading in loops.
  • Unbounded IMemoryCache or static dictionaries used as caches.
  • Span/unsafe micro-optimisations in cold code that hurt readability.
  • Enabling Native AOT without checking library compatibility and trimming warnings.