Transient vs Scoped vs Singleton in .NET — Simple Guide for Developers
Learn when to use Transient, Scoped, or Singleton in .NET. A beginner-friendly guide to Dependency Injection lifetimes with real examples and best practices.
If you’ve ever built a .NET application, you’ve likely heard about Dependency Injection (DI) — that magical system that connects all your services together without you having to manually create objects everywhere.
But there’s one question that often confuses even experienced developers:
“How long should my service live?”
Should it be Transient, Scoped, or Singleton?
At first, this might sound like a small technical setting, but in reality, it can make or break your app’s performance, memory usage, and stability.
Pick the wrong lifetime, and you could end up with memory leaks, weird bugs, or inconsistent data.
Pick the right one, and your app will run clean, fast, and predictable.
Let’s break this down — in plain English — so you’ll always know which lifetime to choose.
🚗 Understanding Service Lifetimes (Think of It Like Renting a Vehicle)
Imagine you’re renting vehicles for different tasks:
| Analogy | Lifetime | Description |
|---|---|---|
| 🚴♂️ A bike for a quick, one-time ride | Transient | Cheap, short-lived |
| 🚗 A car you keep for the entire day | Scoped | Shared during one trip/request |
| 🚌 A bus that keeps running for everyone all day | Singleton | Shared globally |
In .NET, service lifetimes decide how long a service object exists before the DI container replaces or disposes it.
You choose this when registering your service using:
AddTransient()
AddScoped()
AddSingleton()
Each tells .NET how long to keep an instance around — and who gets to reuse it.
💡 Why Service Lifetime Matters
Every time your app injects a service, the DI container must decide whether to:
- 🆕 Create a new instance
- 🔁 Reuse an existing one
- 🌍 Share one across the entire application
That small choice impacts memory usage, performance, and thread safety.
If you choose incorrectly:
- You might reuse data that shouldn’t be shared.
- You might waste memory creating too many objects.
- You might get runtime errors (like injecting a Scoped service into a Singleton).
Choosing the right lifetime means your services behave consistently and efficiently — no surprises in production.
⚡ Transient — “New Every Time”
A Transient service is created every time it’s requested.
No caching, no reuse.
Think of it like getting a brand-new disposable pen every time you need to write something — simple, stateless, and cheap.
🧱 Registering a Transient Service
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
✅ When to Use It
- For stateless services (no internal data to share)
- Lightweight utilities like validators, mappers, or string formatters
- When each call should get a clean instance
⚠️ Watch Out For
- Don’t use it for heavy or expensive services — it’ll slow you down.
- If it uses unmanaged resources, ensure it’s properly disposed.
Use Transient when you don’t care about reusing instances — just quick, clean, and independent.
🔄 Scoped — “One Per Request”
A Scoped service is created once per scope — usually one per HTTP request.
All parts of that request share the same instance, but the next request gets a fresh one.
Think of it like renting a car for a day — you can use it throughout the trip, but it’s returned and reset after.
🧱 Registering a Scoped Service
builder.Services.AddScoped<IUserContext, HttpUserContext>();
✅ When to Use It
- For request-specific logic — like user context or request data
- Services that depend on the current user, transaction, or DbContext
- Anything that needs consistency within a request but not between requests
⚠️ Watch Out For
- Never inject a Scoped service into a Singleton — it’ll crash.
- Don’t assume it’s shared across requests — it’s not.
- Dispose scopes properly if you create them manually.
Scoped is often the default choice in ASP.NET Core web apps because it fits how HTTP requests work.
🌍 Singleton — “One for the Whole App”
A Singleton service is created once and shared everywhere for the entire lifetime of the app.
Think of it like a bus that runs for everyone — all requests, all users — forever.
🧱 Registering a Singleton Service
builder.Services.AddSingleton<IClock, SystemClock>();
✅ When to Use It
- For global, stateless, and thread-safe services
- Logging, configuration providers, or caching layers
- Background tasks or system-wide helpers
⚠️ Watch Out For
- Don’t inject Scoped services into a Singleton.
- Be careful with shared state — if mutable, make it thread-safe.
- Avoid heavy dependencies or disposable objects unless you manage cleanup manually.
Singletons offer great performance — but with great power comes great responsibility.
🧭 Real-World Examples
| Lifetime | Example Service | Why It Fits |
|---|---|---|
| Transient | IEmailBuilder, IPasswordHasher | Cheap, stateless utilities |
| Scoped | DbContext, IUserContext, IUnitOfWork | Request-specific data |
| Singleton | ILogger<T>, IClock, ICacheService | Global, thread-safe logic |
Always ask yourself:
“Does this service hold per-request data, or can it be shared safely?”
That question alone will guide most lifetime decisions.
❌ Common Mistakes (and How to Avoid Them)
| Mistake | Why It’s Bad | How to Fix |
|---|---|---|
| Injecting Scoped into Singleton | Causes runtime errors | Use IServiceScopeFactory to resolve Scoped services |
| Making Singletons Stateful | Causes race conditions | Keep them stateless or thread-safe |
| Overusing Transient | Hurts performance | Use for lightweight, stateless logic only |
| Forgetting to Dispose | Memory leaks | Dispose services you create manually |
| Assuming Scoped Works Everywhere | Breaks in background jobs | Create scopes manually outside HTTP contexts |
🧩 How .NET Handles Lifetimes Internally
Here’s a simplified look under the hood:
- Registration — Each
AddTransient,AddScoped, orAddSingletoncall registers aServiceDescriptorin the DI container. - Resolution — The DI system checks whether to create, reuse, or share an instance.
- Scope — ASP.NET Core creates one scope per HTTP request.
- Disposal — Scoped services are disposed at the end of each request; Singletons on app shutdown.
- Thread Safety — The DI container itself is thread-safe, but your services need to be too.
Most of the time, you won’t need to worry about this — but knowing how it works helps you debug tricky “why is my service reused?” issues.
🏆 Best Practices for Choosing the Right Lifetime
✅ Keep Singletons stateless and thread-safe
✅ Use Scoped for anything tied to a request or unit of work
✅ Use Transient for small, stateless utilities
✅ Avoid mixing lifetimes incorrectly
✅ Dispose of services properly when you create them manually
A good rule of thumb:
| Data Lifespan | Recommended Lifetime |
|---|---|
| Per request | Scoped |
| Global | Singleton |
| Temporary | Transient |
Match the lifetime to the data’s lifespan.
🧾 Summary
Service lifetimes are one of the simplest yet most critical concepts in .NET Dependency Injection.
| Lifetime | Description | Ideal Use |
|---|---|---|
| Transient | New instance every time | Lightweight, stateless services |
| Scoped | One instance per request | Request-specific services |
| Singleton | One instance for the app | Global, stateless, thread-safe services |
Choosing wisely keeps your app fast, reliable, and bug-free — while making your codebase easier to test and maintain.
