CI: merge often, and build and test automatically. CD: every change that passes is releasable (delivery) or released automatically (deployment).
Commit / PR
→
Buildcompile, unit tests
→
ScanSAST · SCA · secrets · IaC
→
Artifact / imagesigned, versioned
→
Dev → Test → SIT → UAT
→
Approval gateCAB / change
→
Prodsafe deployment strategy
Continuous delivery = a manual approval before prod. Continuous deployment = fully automatic.
Build once, deploy the same artifact to every environment. Only the config changes.
DevSecOps / shift-left: security scans run in the pipeline, not after go-live.
DORA metrics: deployment frequency, change lead time, change fail rate, failed deployment recovery time, and (since 2024) deployment rework rate. See 15.15.
How a team shares one codebase: where changes live, how they get reviewed, and how they get back into main quickly and safely.
Three branching models
🏷️ GitFlow
Long-lived main and develop, plus feature, release and hotfix branches. Fits versioned products with several supported releases (SDKs, installed software). Heavy for web services: its author added a 2020 note recommending simpler flows for continuously delivered apps.
🔀 GitHub flow
main is always deployable. Short branch → pull request → review and checks → merge → deploy. A simple, good default for most teams.
🌳 Trunk-based
Everyone integrates into trunk at least daily, directly or through branches that live hours, at most a day or two. Unfinished work hides behind feature flags. This is the model CI/CD assumes.
Model
Long-lived branches
Branch life
Release from
Best for
GitFlow
main, develop (+ release, hotfix)
Days to weeks
Release branch, tagged on main
Products with several supported versions
GitHub flow
main only
Hours to a few days
main, after each merge
Web apps and APIs with one live version
Trunk-based
trunk only
Hours (1–2 days max)
Trunk, or a short release branch cut from it
CI/CD and frequent deploys
Why trunk-based + feature flags suits CI/CD: "continuous integration" means merging with everyone else's code at least daily. A branch that lives two weeks isn't integrated, so conflicts and surprises pile up at the end. Flags split deploy from release, so unfinished code can merge safely. DORA lists trunk-based development as a capability linked to better delivery performance.
Pull requests & code review
🔍 What reviewers look for
Correctness: edge cases, error paths, concurrency
Design: does it fit? Is there a simpler way?
Tests that prove the behaviour, not just coverage
Security: input validation, authorisation, no secrets
Operability: logs, metrics, flags, safe DB migrations
Readability: names, comments that explain why
🤝 Etiquette
Review the code, not the person. Ask questions, don't command
Label optional comments as nit: so blockers stand out
Approve when it improves the codebase, not when it's perfect
Author: small PR, a clear description (why, how tested), self-review first
Respond within one business day at most (Google's guidance)
📏 PR size
Small PRs get faster, better reviews. Google's guide: ~100 lines is usually reasonable, 1,000 is usually too large. Split refactoring from behaviour changes.
⏱️ Review SLA
Agree a first-response target (hours, not days). Waiting for review is often the biggest part of lead time (15.15).
👥 Pairing & mob
Review happens while the code is written. It can replace async PR review where policy allows. Regulated teams may still need a recorded second approver.
🤖 Automate the boring bits
Formatting, linting, tests and security scans run as required checks. Humans review logic and design.
Commits, protected branches & ownership
Conventional Commits (spec 1.0.0): a typed, machine-readable message. Tools use it to pick the next version and write the changelog (15.4).
fix(transfers): reject negative amounts → PATCH
feat(payments): add PayTo mandate status endpoint → MINOR
feat(api)!: remove v1 /accounts route → MAJOR
BREAKING CHANGE: clients must call /v2/accounts
# other types: docs, test, refactor, perf, build, ci, chore
Control
What it does
Protected branch
No direct pushes, force-pushes or deletes on main. GitHub: rulesets / branch protection. Azure Repos: branch policies
Required checks
Build, tests, lint and security scans must pass before merge
Required reviews
1–2 approvals. Dismiss stale approvals when new commits arrive
CODEOWNERS
Auto-requests the owning team for matching paths, e.g. /infra/ → platform team
Merge queue
Tests each PR on top of the latest main before merging, so main stays green
Signed commits
Proves who made a change. Part of supply chain security (14.10)
📦 Monorepo
✅ Atomic changes across services, one toolchain, easy refactoring
✅ Shared code without publishing packages
❌ Needs builds that only test what changed (path filters, Nx, Bazel)
❌ Big clones. CODEOWNERS becomes essential
🗂️ Polyrepo
✅ Clear ownership, permissions and pipelines per service
✅ Small, fast repos
❌ Cross-repo changes need versioned packages and coordination
❌ Duplicated pipeline config, dependency drift
Integrate small changes at least daily. Long-lived branches are merge pain you've postponed.
Trunk-based + feature flags: deploy any time, release when ready.
Small PRs, quick reviews. Automate style so humans review logic.
Protect main with required checks, required reviews and CODEOWNERS.
.NET 10 How to implement it
Library / API
Package
Use it for
Format check
built-indotnet format --verify-no-changes
A required PR check: fails if code doesn't match .editorconfig
One rule set for every project via Directory.Build.props. Warnings block the merge
Central package versions
built-inDirectory.Packages.props
One place for NuGet versions in a monorepo, so dependency PRs are small and clear
<!-- Directory.Build.props at the repo root: applies to every project -->
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>
</Project>
Long-lived branches are merge conflicts you haven't met yet 🌿
🌳 Trunk-based: integrate at least daily, hide unfinished work behind flags
Many fast, cheap tests at the bottom, a few slow, realistic ones at the top, and some checks that only make sense in production.
🔺 Test pyramid
Lots of unit tests, fewer integration tests, very few end-to-end tests. Fast feedback, cheap to run.
🏆 Testing trophy
Kent C. Dodds' version: static analysis at the base, some unit tests, the most integration tests, a few E2E. Integration tests give the most confidence per minute.
Both shapes say the same thing: buy the most confidence for the least test time. The anti-pattern is the "ice-cream cone": mostly manual and E2E tests.
Contract testing (consumer-driven)
Consumer test"I call GET /accounts/42 and need id + balance"
→
Pact filethe contract
→
Pact Brokershares contracts and results
→
Provider verificationin the provider's CI
→
can-i-deploy ✓
Each consumer states what it actually uses. The provider proves it still meets every contract before it deploys.
It replaces most slow, flaky cross-team E2E tests between microservices.
Performance testing: four different questions
Type
Question it answers
Banking example
Load
Do we meet our SLOs at the expected peak?
Lunchtime card traffic
Stress
Where does it break, how does it fail, and does it recover?
2× the busiest day on record
Spike
Can we survive a sudden surge? Does autoscaling react fast enough?
Payday at midnight, a viral sale
Soak (endurance)
Does it degrade over hours? Memory leaks, connection pools, full disks
A 12-hour run before a major release
Chaos engineering
🧪 Hypothesis first
"If one zone fails, p99 latency stays under 500 ms and no payment is lost." Then inject the failure and measure.
💥 Small blast radius
Start with one instance, in staging, or 1% of traffic. Always have an abort switch.
🎮 Game days
Planned team exercises that break things on purpose, and practise the incident response too.
Inject faults and delays into a resilience pipeline to prove your retries and timeouts work
// xUnit v3: the real API in memory + a real Postgres in Docker
public sealed class PaymentsApiTests : IAsyncLifetime
{
private readonly PostgreSqlContainer _db = new PostgreSqlBuilder("postgres:17").Build();
private WebApplicationFactory<Program> _factory = null!;
public async ValueTask InitializeAsync()
{
await _db.StartAsync();
_factory = new WebApplicationFactory<Program>().WithWebHostBuilder(b =>
b.UseSetting("ConnectionStrings:Payments", _db.GetConnectionString()));
}
[Fact]
public async Task Create_payment_returns_201()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/payments",
new { FromAccount = "123", ToAccount = "456", Amount = 50m },
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
public async ValueTask DisposeAsync()
{
await _factory.DisposeAsync();
await _db.DisposeAsync();
}
}
Testcontainers needs Docker on the build agent. Polly chaos strategies are enabled as soon as you add them: drive Enabled / EnabledGenerator from config so they can never fire in production by accident.
Tests should buy confidence, not a coverage percentage 🧪
🔺 Many fast unit tests, fewer integration tests, a handful of E2E.
🤝 Contract tests (Pact) instead of fragile cross-team E2E suites.
🏋️ Load, stress, spike and soak tests answer four different questions.
🐒 Chaos engineering: start with a hypothesis and a small blast radius.
🔭 Shift right too: synthetic monitoring and canary analysis in production.
What's the slowest test in your pipeline, and has it ever caught a bug?
#Testing #DevOps #QualityEngineering #SRE
15.4 Versioning & release management Added
A version number is a promise about compatibility. Release management decides how a tested artifact gets that number, its release notes, and its path to production.
Semantic Versioning (SemVer 2.0.0)
Part
Change it when
Example
MAJOR
Consumers could break (removed member, changed contract)
NuGet picks the lowest version that satisfies a range, so bump honestly.
0.y.z means "no stability promise yet".
Catch accidental breaking changes in the build with package validation (below).
🌐 HTTP APIs
Usually expose only the MAJOR: /v1, /v2 (styles in 8.2).
Within a major, changes must be additive: new optional fields, new endpoints.
Publish a deprecation date and use the Sunset header before removing a version.
Contract tests catch breaking changes before release (15.3).
Release trains vs continuous delivery
🚂 Release train
A fixed cadence (fortnightly, monthly, quarterly). Whatever is ready catches the train. ✅ Predictable for the business, change boards, app stores and multi-team releases. ❌ Big batches, waiting, more risk per release.
🔁 Continuous delivery
Every merged change can go to production on its own, often behind a flag. ✅ Small risk per change, fast feedback. ❌ Needs strong test automation, observability and rollback.
Many banks run both: continuous delivery for internal services and APIs, and a train for mobile app releases and core banking changes.
Build once, promote the same artifact
Merge to main
→
CI builds 1.8.0image digest + signature + SBOM
→
Registry / feedimmutable
→
Dev → Test → Pre-prodsame digest, different config
→
Prodafter the approval gate
Artifact immutability: a published version never changes. Never rebuild or overwrite 1.8.0; ship 1.8.1. nuget.org won't accept the same version twice (you can only unlist).
Deploy containers by digest (@sha256:…), not a moving tag like latest.
Environment promotion moves the artifact you tested, never a fresh build. Config and secrets come from each environment (15.1).
Keep provenance: who built it, from which commit, signed and with an SBOM (14.10).
Changelogs & release notes
📜 Changelog (for developers)
Every notable change per version, grouped as Added, Changed, Deprecated, Removed, Fixed, Security ("Keep a Changelog" style). Lives in the repo.
📣 Release notes (for users)
What changed for them: new features, migration steps for breaking changes, known issues. Written for the audience, not a list of commits.
Automate it: Conventional Commits (15.2) let tools such as release-please or semantic-release work out the next version, write the changelog and create the release. In .NET, MinVer, Nerdbank.GitVersioning or GitVersion stamp the version into the build.
A breaking change means a new MAJOR. No exceptions, even for "tiny" ones.
Published artifacts are immutable. Promote the same one, by digest, through every environment.
Derive versions and changelogs from git (tags, commit messages), not by hand.
Release trains trade speed for predictability. Continuous delivery trades process for automation.
.NET 10 How to implement it
Library / API
Package
Use it for
Version from git tags
MinVer
Tag v1.5.0 → version 1.5.0. Commits after the tag get a pre-release version. Minimal config
Version from a file
Nerdbank.GitVersioning
version.json holds major.minor; git height adds the patch. Hosted in the dotnet GitHub org
Version from branch rules
GitVersion.MsBuild (or the GitVersion.Tool CLI)
Calculates SemVer from branches, tags and commit messages. Fits GitFlow-style repos
Pack & publish
built-indotnet pack, dotnet nuget push
Build the .nupkg (plus .snupkg symbols) and push it to nuget.org, Azure Artifacts or GitHub Packages
Source Link & deterministic builds
built-in Source Link (SDK 8+), ContinuousIntegrationBuild
Debuggers can step into the exact commit's source. The same source gives the same binary
Git-based versioning needs tags and history. A shallow CI clone gives wrong versions: use fetch-depth: 0 in actions/checkout. Only set ContinuousIntegrationBuild on CI, or local debugging can't find your source files.
1.4.2 → 2.0.0 is a promise, not a vibe 🏷️
🔢 MAJOR = breaking, MINOR = new feature, PATCH = fix
🧊 Never overwrite a published version. Ship 1.4.3 instead
📦 Build once, then promote the same artifact by digest
🤖 Let git tags and Conventional Commits pick the version and write the changelog
🚂 Release train or continuous delivery? Predictability vs small batches
When did a "minor" update last break your build?
#SemVer #ReleaseManagement #DevOps #dotnet #NuGet
15.5 Deployment strategies
Ways to release a new version that trade off downtime, risk, cost and rollback speed.
1. Recreate (big bang)
Stop all, start the new version. ➕ Simple, cheap. ➖ Downtime, risky.
2. Rolling
Replace instances in batches. ➕ No downtime, no extra cost. ➖ Mixed versions run during rollout, slow rollback.
3. Blue-Green
Two full environments, flip the switch. ➕ Instant switch and rollback. ➖ 2× cost. DB schema must work with both versions.
4. Canary
Expose to a small slice first. ➕ Real-traffic validation, small blast radius. ➖ Needs good monitoring and traffic splitting.
5. A/B testing
Like canary, but for business experiments, not technical safety.
6. Shadow / dark launch
Test v2 with real production traffic and no user impact. ➖ Watch out for side effects (don't double-charge!).
Strategy
Downtime
Risk
Extra cost
Rollback
Complexity
Recreate
Yes
High
None
Slow (redeploy)
Low
Rolling
No
Medium
Low
Medium
Low
Blue-Green
No
Low
High (2×)
Instant
Medium
Canary
No
Lowest
Low–medium
Fast
High
A/B
No
Low
Medium
Fast
High
Shadow
No
None to users
High
N/A
High
Database changes are the hard part. Use expand → migrate → contract so old and new code both work with the schema.
Define automatic rollback triggers (error rate, p99 latency) for canary and blue-green.
Ring-based deployment: internal users → early adopters → region → everyone (used by Microsoft).
Roll back vs roll forward: rolling back is safest if it's fast. Roll forward when a fix is small and well understood.
.NET 10 How to implement it
Strategy
Library / API
Notes
Rolling
Kubernetes Deployment / App Service; built-in health checks + HostOptions.ShutdownTimeout
The platform does the batches. Your app must report readiness and shut down gracefully
Blue-green
App Service deployment slots (swap), Container Apps revisions
Platform features; script them with Azure.ResourceManager.AppService or the az CLI
Canary / ring
Slot or revision traffic %, Azure Front Door weights, Argo Rollouts (not .NET); in code: Microsoft.FeatureManagement targeting / percentage filters
Shift traffic in steps, or turn a feature on for rings of users
A/B
Microsoft.FeatureManagement variants + telemetry
Split users between variants and compare business metrics
Shadow
Service mesh traffic mirroring (platform)
No standard .NET library. Make the shadow copy side-effect free
Database changes
EF Core migrations (dotnet ef migrations bundle)
Run expand/contract migrations as a separate pipeline step, not at app startup
6 deployment strategies, ranked by how well you'll sleep 😴
😱 Recreate: everything down, fingers crossed
😐 Rolling: batch by batch
🙂 Blue-Green: flip a switch, flip it back
😌 Canary: 5% first, watch, then grow
🧪 A/B: test the business idea
🕶 Shadow: real traffic, zero user impact
Deploy ≠ release. Feature flags let you do both separately.
#DevOps #CICD #SRE
15.6 Feature flags (toggles) Added
Ship code turned off, then turn it on for chosen users without redeploying. Deploy ≠ release.
Release toggle
Hide unfinished features. Enables trunk-based development (15.2).
Experiment toggle
A/B tests.
Ops toggle / kill switch
Turn off a feature that's causing trouble.
Permission toggle
Premium or beta users only.
Remove old flags. Flag debt becomes untestable code paths. Tools: LaunchDarkly, Azure App Configuration, Unleash.
.NET 10 How to implement it
Library / API
Package
Use it for
Feature Management
Microsoft.FeatureManagement.AspNetCore
IFeatureManager, [FeatureGate], percentage / time-window / targeting filters and variants. Flags can live in appsettings.json
Describe the app and its resources in C#; azd generates Bicep and provisions Azure
Pulumi
Pulumi, Pulumi.AzureNative
Infrastructure in C# with real loops, types and tests
Azure SDK management
Azure.ResourceManager, Azure.ResourceManager.*
Imperative automation and tooling (drift checks, clean-up jobs)
GitOps
Argo CD / Flux (not .NET)
Pull your manifests from Git into the cluster
// Aspire AppHost: the app and its infrastructure in one C# file
var builder = DistributedApplication.CreateBuilder(args);
var db = builder.AddAzurePostgresFlexibleServer("pg").AddDatabase("orders");
builder.AddProject<Projects.Orders_Api>("api").WithReference(db);
builder.Build().Run();
// locally: runs everything with the dashboard; "azd up": provisions Azure and deploys
15.8 Containers & Kubernetes Added
A container packages an app with its dependencies. Kubernetes runs many containers across many machines: it schedules, heals, scales and connects them.
VM vs container
VM = [App][Libs][Guest OS] on a hypervisor. Container = [App][Libs] on the shared host OS kernel → lighter, starts in seconds.
Kubernetes at a glance
Deployment → ReplicaSet → Pods (1+ containers). Service = stable IP/DNS for pods · Ingress = L7 routing in · ConfigMap / Secret = config · HPA = auto-scale pods. Control plane: API server · scheduler · controller manager · etcd (state). Worker nodes: kubelet + container runtime.
Probes: liveness (restart if dead), readiness (send traffic only when ready), startup.
StatefulSet for stateful apps, DaemonSet for one pod per node (agents).
Generate deployment artifacts for Kubernetes or Azure Container Apps from the AppHost
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
.AddDbContextCheck<AppDbContext>(tags: ["ready"]); // Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore
builder.Services.Configure<HostOptions>(o => o.ShutdownTimeout = TimeSpan.FromSeconds(25));
var app = builder.Build();
app.MapHealthChecks("/health/live", new() { Predicate = r => r.Tags.Contains("live") }); // restart if failing
app.MapHealthChecks("/health/ready", new() { Predicate = r => r.Tags.Contains("ready") }); // no traffic if failing
Don't check the database in the liveness probe. A DB outage would make Kubernetes restart every pod, which doesn't help.
15.9 Observability Added
Monitoring tells you that something is wrong. Observability lets you ask why, from the telemetry the system emits.
📜 Logs
Timestamped events. Make them structured (JSON) and include a correlation ID. Never log secrets or PII.
📈 Metrics
Numbers over time: request rate, error %, latency, CPU. Cheap to keep, good for alerts.
🧵 Traces
The path of one request across services (spans with timings). Shows where the time went.
The W3C traceparent header is propagated on every call, so all these spans share one trace ID (4bf9…). Child spans (└) sit inside their parent's time. Here the SQL INSERT is the bottleneck.
Your own spans. W3C traceparent is propagated automatically by HttpClient
Aspire dashboard
Aspire (also runs as a standalone container)
Local OTLP viewer for logs, traces and metrics
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("orders-api"))
.WithTracing(t => t.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSource("Orders")) // our own ActivitySource
.WithMetrics(m => m.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddMeter("Orders", "System.Runtime")) // our Meter + runtime metrics
.WithLogging()
.UseOtlpExporter(); // endpoint from OTEL_EXPORTER_OTLP_ENDPOINT
// a custom span (Source is a static field: new ActivitySource("Orders"))
using var activity = Source.StartActivity("PlaceOrder");
activity?.SetTag("order.channel", "web");
15.10 Logging Added
A log is a timestamped record of something that happened. Logs explain why something went wrong, and they're only useful if you can search and correlate them.
App / API / Functionemits structured logs
→
Agent / SDKOpenTelemetry, App Insights SDK, Fluent Bit
→
Central log storeLog Analytics · Splunk · ELK · Datadog
→
Search · dashboards · alerts
→
Archivecheap storage for retention
Log levels: use them consistently
Level
When
Example
FATAL / CRITICAL
The app can't continue
Can't connect to DB at startup
ERROR
An operation failed and needs attention
Payment 7f3a failed: downstream 503 after 3 retries
WARN
Unexpected, but handled
Retry 1/3 to fraud service, circuit half-open
INFO
Normal business milestones
Payment 7f3a accepted, amount band B
DEBUG / TRACE
Diagnostic detail. Off in prod by default
Request mapping, SQL parameters (masked)
Structured vs unstructured
✗ Unstructured "Payment failed for cust 42 amount 800 because timeout"
✓ Structured {"ts":"2026-10-16T01:02:03Z","level":"ERROR","service":"payments","env":"prod",
"traceId":"4bf92f…","correlationId":"c-91a…","event":"PaymentFailed",
"paymentId":"7f3a","reason":"FraudServiceTimeout","durationMs":2004}
→ query it: where event=="PaymentFailed" | summarize count() by reason, bin(ts, 5m)
🧵 Correlation
Put traceId / correlationId on every log line and pass it across services (W3C traceparent), so you can follow one request end to end.
🏷 Context fields
service, version, environment, region, instance, operation, user or tenant ID (pseudonymised).
🔒 Never log
Passwords, tokens, secrets, full card numbers (PAN), CVV, full PII. Mask them (****1234) or tokenise.
💰 Volume & cost
Sample noisy DEBUG and success logs, set retention tiers (hot 30 d → archive 1–7 yrs), and drop health-check spam.
📜 Audit logs ≠ app logs
Audit logs record who did what, and when. Keep them immutable and tamper-evident, with longer retention, for compliance.
⏱ Async & non-blocking
Logging must never slow down or crash the request. Use buffered, async sinks.
Log events and decisions, not every line of code. Ask: "Would this help someone at 3 a.m.?"
Log an error once, at the boundary where it's handled. Logging it at every layer creates duplicate noise.
Use UTC timestamps in ISO 8601 format.
Make the log level changeable at runtime, so you can turn on DEBUG briefly during an incident.
.NET 10 How to implement it
Library / API
Package
Use it for
ILogger
built-inMicrosoft.Extensions.Logging
Structured logs with message templates ({PaymentId}), levels set per category in config
Source-generated logging
built-in[LoggerMessage]
Fast, allocation-free log methods with fixed event names
Scopes & correlation
built-inBeginScope; trace and span ids come from Activity
Put correlation id, tenant and trace id on every line
Tag PII with data-classification attributes; AddRedaction() + EnableRedaction() mask it in logs
Shipping logs
OpenTelemetry (WithLogging), or Serilog.AspNetCore + sinks, or NLog.Web.AspNetCore
Send JSON logs to Log Analytics, Splunk, ELK or Datadog
public static partial class Log
{
[LoggerMessage(LogLevel.Error, "Payment {PaymentId} failed: {Reason}")]
public static partial void PaymentFailed(this ILogger logger, string paymentId, string reason);
}
// every log line inside the scope carries CorrelationId
using (logger.BeginScope(new Dictionary<string, object> { ["CorrelationId"] = correlationId }))
{
logger.PaymentFailed("7f3a", "FraudServiceTimeout"); // fields, not one long string
}
Don't use string interpolation ($"Payment {id}") in log calls. It loses the structured fields and always builds the string, even when the level is off.
Good logs are a gift to your future on-call self 🎁
✅ Structured JSON, not free text
✅ A traceId on every line
✅ Consistent levels (ERROR means someone must act)
❌ Never tokens, passwords or card numbers
❌ Not the same error logged at 5 layers
If you can't query it, it's not a log. It's a diary.
#Observability #Logging #SRE #DevOps
15.11 Metrics Added
Metrics are numbers measured over time. They're cheap to store, fast to query, and ideal for dashboards, trends and alerts. They tell you that something is wrong.
Metric types
➕ Counter
Only goes up (resets on restart). Requests, errors, payments processed. Look at its rate: requests/s.
🌡 Gauge
Goes up and down. CPU %, memory, queue depth, active connections.
📊 Histogram
Counts values in buckets, so you can calculate percentiles (p50, p95, p99). Latency, payload size.
📐 Summary
Percentiles calculated on the client. Can't be combined across instances, so prefer histograms.
What to measure
Layer
Framework
Example metrics
User-facing service / API
RED: Rate, Errors, Duration
req/s, 5xx %, p95/p99 latency per endpoint
Infrastructure resource
USE: Utilisation, Saturation, Errors
CPU %, memory, disk I/O wait, thread-pool queue, DB DTU %
Whole system
Golden signals: Latency, Traffic, Errors, Saturation
A combination of the above
Messaging
Flow health
Queue depth, message age, consumer lag (Kafka), DLQ count
Business (KPIs)
Outcomes
Payments/min, success %, onboarding drop-off, $ value processed
GenAI / LLM
AI ops
Tokens in/out, cost per request, time to first token, groundedness score, 429s
100 requests: 95 take 100 ms and 5 take 3,000 ms. The average (245 ms) looks fine and hides the slow tail. p95 = 100 ms, but p99 = 3,000 ms: 1 in 20 users waits 3 s.
Cardinality kills metric systems. Never use userId, paymentId or full URLs as labels. Use bounded values: endpoint template, status class, region.
Track business metrics next to technical ones. "CPU fine, but payments/min dropped 40%" is the alert that matters.
Tools: Prometheus + Grafana, Azure Monitor metrics, App Insights, Datadog, Dynatrace, CloudWatch. Emit them through OpenTelemetry.
.NET 10 How to implement it
Library / API
Package
Use it for
Metrics API
built-inSystem.Diagnostics.Metrics
Counter<T>, Histogram<T>, UpDownCounter<T>, Gauge<T> (.NET 9+), and Observable* versions
IMeterFactory
built-in (registered by the host)
Create meters through DI so they're testable
Built-in metrics
built-in ASP.NET Core, HttpClient, runtime (System.Runtime)
RED metrics (request rate, errors, duration) and GC / thread-pool stats for free
Collect dumps and traces from pods over HTTP, or automatically when a trigger fires
Kill switch
Microsoft.FeatureManagement
Turn off the failing feature as a fast mitigation, with no redeploy
15.14 SRE practices Added
Site Reliability Engineering runs operations as a software problem: agree how reliable a service must be, spend the leftover risk on purpose, and automate the repetitive work away.
SLI vs SLO vs SLA and the error-budget maths are in 3.10. Burn-rate alerts are in 15.12. This section covers the team practices around them.
Error budget policy
A written agreement, signed off by product and engineering before anything goes wrong, saying what happens as the budget runs down.
Define thresholds and actions, who decides, and the exceptions (security patches, legal deadlines).
Name an escalation path for disagreements (e.g. head of engineering).
If the budget is always untouched, the SLO may be too loose, or you could take more risk. If it's always gone, renegotiate the SLO or invest in reliability.
Toil and the 50% rule
🔁 What toil is
Manual, repetitive, automatable work with no lasting value that grows with the service. E.g. restarting a stuck job, renewing certificates by hand, re-running failed batches.
⚖️ The 50% cap
Google SRE caps operational work at 50% of each engineer's time. The rest goes to engineering that removes toil or improves reliability. Over the cap? Hand work back to the dev team.
🛠️ Reduce it
Measure it (tickets, pages, hours). Automate the most frequent task first, offer self-service, fix the root cause, delete useless alerts.
On-call health
Practice
Why
Big enough rotation
Google SRE: at least 8 people for a single-site 24×7 primary + secondary rota, or 6 per site with follow-the-sun
Page only on symptoms
Users hurting or the SLO burning (15.12). Causes go to dashboards and tickets
Runbook on every page
The responder knows the first steps at 3 a.m.
Cap the load
Google aims for at most 2 incidents per 12-hour shift. More means fix the system, not hire heroes
Written handover
At each rotation change: open incidents, silenced alerts, risky changes in flight
Recovery & review
Time off after a bad night. Review pages per week and false positives every month
Blameless postmortems
Blameless: assume people acted sensibly with what they knew. Fix the system, not the person. Blame makes people hide information.
Triggers: any Sev 1/2, data loss, a big error-budget burn, or an incident that monitoring missed.
Share postmortems widely and review open action items regularly. An unfinished action item is a repeat incident waiting to happen.
Production readiness review (PRR)
A checklist a service passes before it goes live or before SRE takes on its pager.
📈 Observability
SLIs and SLOs defined, dashboards, alerts with runbooks, distributed tracing.
🛡️ Resilience
Timeouts, retries, circuit breakers (3.6), load tested, DR tested (3.13).
🚀 Deploy & rollback
Safe deployment strategy, flags, a tested rollback, backward-compatible DB migrations.
🔐 Security & capacity
Threat model, secrets in a vault, capacity plan with headroom.
📟 Ownership
On-call rota, escalation contacts, support docs, dependencies listed.
Game days
Planned exercises that inject a real failure (zone outage, slow dependency, expired certificate) while the on-call team responds as if it were live. They test runbooks, alerts and people, not just code. How to run them, plus chaos principles and tools, is in 15.16.
SLOs and error budgets turn "features vs reliability" into an agreed, data-driven rule.
Agree the error budget policy in advance, not in the middle of an incident.
Keep toil under 50%. Automate the most frequent manual task first.
Page on symptoms with a runbook. Write blameless postmortems with owned action items.
// SLO: 99% of requests faster than 300 ms, so 0.3 s must be a bucket edge
builder.Services.AddOpenTelemetry().WithMetrics(m => m
.AddAspNetCoreInstrumentation()
.AddView("http.server.request.duration",
new ExplicitBucketHistogramConfiguration
{
Boundaries = [0.05, 0.1, 0.3, 0.5, 1, 2.5, 5]
}));
// SLI = requests with duration <= 0.3 s / all requests, over 30 days (PromQL / KQL)
If no bucket edge sits on the SLO threshold, the backend has to interpolate, and your "under 300 ms" SLI is only an estimate.
Reliability is a feature with a budget 💰
🎯 Agree an SLO with the business. "100% uptime" isn't a target
🔁 Toil over half the week? Automate before taking on more
📟 Page only when users hurt, and always with a runbook
📝 Blameless postmortems: fix the system, not the person
What does your team do when the error budget runs out?
#SRE #Reliability #OnCall #DevOps #Observability
15.15 DORA metrics & engineering effectiveness Added
DORA's research-backed metrics measure software delivery outcomes, how fast and how safely changes reach production, so teams can see whether their improvements work.
The five metrics (DORA, 2024 onwards)
🚀 Throughput
Change lead time: from commit to running in production.
Deployment frequency: how often you deploy to production.
Failed deployment recovery time: how long to recover from a deployment that needed immediate intervention. It replaced MTTR in 2023.
💥 Instability
Change fail rate: % of deployments that need immediate intervention (rollback or hotfix).
Deployment rework rate (added 2024): % of deployments that are unplanned, made to fix a production incident.
These are the original "four keys" plus rework rate. DORA keeps finding that speed and stability aren't a trade-off: top performers do well on all of them. The lever behind all five is small batches.
Performance clusters
2024 cluster
Lead time
Deploy frequency
Change fail rate
Recovery time
Share
Elite
< 1 day
On demand (many a day)
5%
< 1 hour
19%
High
1 day – 1 week
Daily to weekly
20%
< 1 day
22%
Medium
1 week – 1 month
Weekly to monthly
10%
< 1 day
35%
Low
1 – 6 months
Monthly to 6-monthly
40%
1 week – 1 month
25%
Latest (2025 DORA report): DORA dropped the single elite-to-low ladder. It now gives benchmarks per metric and describes seven team archetypes, from "Foundational challenges" to "Harmonious high-achievers", that also factor in well-being, friction and product performance. DORA's advice: aim for elite improvement, not an elite label.
Measuring them from pipelines and incidents
Gitcommit times, PRs
→
Pipelinedeploy events: service, version, SHA
→
Incident toolincident ↔ deploy link
→
Metrics store
→
Team dashboardtrend per service
Metric
Data source
Calculation
Deployment frequency
Production deploy events
Deploys per day or week, per service
Change lead time
Commit time + the deploy that shipped it
Median of deploy time − commit time
Change fail rate
Deploys linked to rollbacks, hotfixes or incidents
Failed deploys ÷ all deploys
Recovery time
Incident opened → resolved, for change-caused incidents
Median duration
Rework rate
Deploys tagged unplanned / hotfix
Unplanned deploys ÷ all deploys
Tag every deploy with service, version, commit SHA and environment. Link each incident to the change that caused it.
Measure one service at a time, use medians, and watch trends over months.
Start with a conversation or the free DORA Quick Check (dora.dev) before building a big data pipeline.
SPACE framework in brief
Forsgren, Storey and others (2021): developer productivity can't be one number. Pick metrics from at least three of these dimensions, mixing surveys and system data.
😊 Satisfaction & well-being
Fulfilment, burnout, tooling happiness (surveys).
🎯 Performance
Outcomes: quality, reliability, customer impact.
📊 Activity
Counts of PRs, builds, deploys. Never use them alone.
A counter for deployments and a histogram for lead time, tagged by service and result
GitHub data
Octokit
Deployments, workflow runs, pull requests and commits through the GitHub REST API
Azure DevOps data
Microsoft.TeamFoundationServer.Client
Builds, pipeline runs and work items. Latest stable is 20.256.x; newer builds are preview
Incident data
HttpClient + Microsoft.Extensions.Http.Resilience
Call your incident tool's REST API and join incidents to deploys by change ID
var meter = new Meter("Delivery"); // or IMeterFactory in a DI-based app
var deploys = meter.CreateCounter<long>("delivery.deployments");
var leadTime = meter.CreateHistogram<double>("delivery.change_lead_time", unit: "h");
// the pipeline POSTs here after every production deployment
app.MapPost("/deployments", (DeploymentEvent e) =>
{
var tags = new TagList { { "service", e.Service }, { "result", e.Result } };
deploys.Add(1, tags);
foreach (var committedAt in e.CommitTimes)
leadTime.Record((e.DeployedAt - committedAt).TotalHours, tags);
return Results.Accepted();
});
// add .AddMeter("Delivery") to OpenTelemetry. Change fail rate =
// deployments{result="failed"} / deployments, calculated in the dashboard
record DeploymentEvent(string Service, string Result, DateTimeOffset DeployedAt,
DateTimeOffset[] CommitTimes);
Speed vs stability is a false choice 📊
🚀 Deployment frequency and lead time: how fast changes flow
🛟 Failed deployment recovery time: how fast you bounce back
💥 Change fail rate and rework rate: how often deploys hurt
📦 The lever behind all five: smaller batches
🚫 Never use them to rank teams. Goodhart's law wins every time
Which metric would move first if your PRs were half the size?
Break things on purpose, in a controlled experiment, to prove that the system and the team cope the way you think they do.
The short version is in 15.3. SLOs, error budgets and the error budget policy are in 15.14. This section is the how-to.
The experiment loop
Steady statenormal SLIs
→
Hypothesis"p99 stays < 500 ms"
→
Inject faultsmall blast radius
→
Observeabort if SLO burns
→
Fix & repeatthen automate
How it works
Part
What it means
Steady state
Measurable "normal": success rate, p99, payments per minute
Hypothesis
"If X fails, steady state holds." Written down before you start
Blast radius
Who can get hurt: one pod, 1% of traffic, one tenant, one zone
Abort conditions
Agreed stop rules: burn rate, error %, a stop button. Automate them
Duration
Faults switch off on their own when time runs out or on abort
Common experiments
🐢 Latency
Add 1-3 s to a dependency. Do timeouts fire, and do retries make it worse?
🔌 Dependency failure
Return 503s, drop the cache, block DNS. Does the fallback or circuit breaker work?
🖥️ Instance / zone loss
Kill pods or VMs, take a zone down, fail over the database. Does traffic move?
📈 Resource exhaustion
CPU or memory pressure, full disk, a drained connection pool. Does autoscaling or shedding kick in?
Non-production first, then production
Code and CI: in-process faults (Polly chaos) in integration tests.
Staging: platform faults with production-like load.
Production: off-peak, tiny blast radius, people watching, abort automated. Only production shows the real config, traffic and dependencies.
Continuous: proven experiments run on a schedule or in the pipeline.
Game days and the error budget
Game day: a planned exercise. A facilitator injects the fault; on-call responds with the real alerts and runbooks; a scribe times detection and mitigation.
Afterwards: blameless review, fix the runbooks and alerts, add action items.
Error budget: experiments spend it on purpose. No budget left? No chaos. Use the burn rate as the abort condition.
Tools
Where
Tool
Azure
Azure Chaos Studio (Workspaces preview; Experiments classic)
AddResilienceHandler on an HttpClient: retry, timeout, then chaos in the same pipeline
Microsoft.Extensions.Resilience (10.x)
Enriches Polly telemetry; brings Polly.Extensions (AddResiliencePipeline) for non-HTTP calls
built-inIConfiguration
Switch chaos on per environment or game day
// Chaos on one dependency, off unless config turns it on (staging or a game day)
bool chaosOn = builder.Configuration.GetValue<bool>("Chaos:Enabled");
builder.Services.AddHttpClient<FxRatesClient>(c => c.BaseAddress = new Uri("https://fx.example.com"))
.AddResilienceHandler("fx", pipeline =>
{
// 1. Normal resilience: this is what the experiment tests
pipeline.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 3 });
pipeline.AddTimeout(TimeSpan.FromSeconds(2)); // per try
// 2. Chaos goes last, closest to the real call
if (chaosOn)
{
pipeline.AddChaosLatency(0.10, TimeSpan.FromSeconds(3)); // 10% of calls +3 s
pipeline.AddChaosOutcome(0.05, () =>
new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)); // 5% get a 503
}
});
// Hypothesis: retries + a 2 s per-try timeout keep FX success above 99.5%
Polly v8 chaos strategies are on as soon as you add them. Gate them with config, or use EnabledGenerator / InjectionRateGenerator to target test users only. Watch the Chaos.OnLatency and Chaos.OnOutcome telemetry events.
Your retries have never been tested. Not really 🧪
📏 Define steady state: success rate, p99, payments per minute
🤔 Write the hypothesis before you break anything
🎯 Smallest blast radius, automatic abort on SLO burn rate
🎮 Game days test people, alerts and runbooks, not just code
💸 Chaos spends error budget. No budget, no experiments
When did your team last fail over a zone on purpose?
Blue-green switches all traffic at once between two full environments, with instant rollback and double cost. Canary shifts traffic gradually and validates on real users with a smaller blast radius, but needs strong monitoring.
How do you deploy a DB schema change with zero downtime?
Expand/contract: add the new column or table (backward compatible), deploy code that writes both, backfill, switch reads, then remove the old structure in a later release.
Test pyramid vs testing trophy?
The pyramid says: many unit tests, fewer integration tests, very few E2E. The trophy (Kent C. Dodds) adds static analysis at the base and puts the most weight on integration tests, because they give the most confidence per minute. Both reject the "ice-cream cone" of mostly manual and E2E tests.
What is consumer-driven contract testing?
Each consumer writes a test that records exactly what it needs from a provider API (a pact). The provider runs those pacts in its own pipeline and can only deploy if every consumer's contract still passes. It catches breaking API changes without a shared, flaky end-to-end environment.
Logs vs metrics vs traces?
Metrics tell you something is wrong (error rate is up). Traces show where (which hop is slow). Logs explain why (the exception details).
What should you alert on?
User-facing symptoms: error rate, latency against the SLO (burn rate), failed synthetic journeys, missing heartbeats, and business KPIs dropping. Show causes like CPU or memory on dashboards, and ticket them rather than paging.
Why are high-cardinality metric labels a problem?
Each unique label combination is a separate time series. Labels like userId or paymentId create millions of series, which blow up memory, cost and query time. Put those IDs in logs and traces instead.
How do you reduce alert fatigue?
Page only on actionable symptoms, add duration windows, use multi-window burn-rate alerts, group and deduplicate, attach runbooks, and delete or downgrade alerts nobody acts on.
Trunk-based development vs GitFlow?
GitFlow keeps long-lived develop and release branches. It suits versioned products with several supported releases, but merges arrive late and big. Trunk-based development merges small changes into trunk at least daily and hides unfinished work behind feature flags, which is what CI/CD needs. Most web teams use trunk-based or GitHub flow.
What makes a good pull request and code review?
A small PR (around a few hundred lines at most) with a clear description of why and how it was tested. Automated checks cover style, tests and security, so reviewers focus on correctness, design, tests, security and operability. Reviews are kind, label nits as optional, and get a response within a business day.
How do you version a NuGet library or an API?
SemVer: MAJOR for breaking changes, MINOR for backward-compatible features, PATCH for fixes. Derive the version from git (MinVer, Nerdbank.GitVersioning or GitVersion), turn on package validation to catch accidental breaks, and never overwrite a published version. HTTP APIs usually expose only the major (/v2) and keep changes within it additive.
Why build once and promote the same artifact?
What reaches production is exactly what was tested. Rebuilding per environment can pull different dependencies or compiler output. Promote one immutable, signed artifact (by image digest) and only change config per environment.
What is an error budget policy?
A pre-agreed rule between product and engineering about what happens as the error budget (100% − SLO) is used: ship normally while there's budget, slow down as it burns, freeze features and fix reliability when it's gone. Exceptions and escalation are written down in advance.
What goes into a blameless postmortem?
Summary, impact, a timestamped timeline, contributing factors (not a person to blame), what went well, badly and where we got lucky, and action items with owners and due dates. The goal is to fix the system so the same failure can't recur.
What are the DORA metrics today?
Five since 2024: change lead time, deployment frequency and failed deployment recovery time (throughput), plus change fail rate and deployment rework rate (instability). The 2025 report replaced the elite/high/medium/low clusters with per-metric benchmarks and seven team archetypes.
How can DORA metrics be misused?
By ranking teams, tying them to individual performance reviews, or setting them as top-down targets. Teams then game them, for example by splitting deploys or not recording incidents. Use them for a team's own improvement over time, together with SLOs and well-being signals (SPACE).
How do you run a chaos experiment safely in production?
Define steady state (success rate, p99) and write a hypothesis first. Prove it in staging. Then run in production off-peak with the smallest useful blast radius (one pod, 1% of traffic, one tenant), automated abort conditions such as an SLO burn-rate alarm, people watching, and only while error budget remains.
Chaos experiment vs game day?
A chaos experiment tests a technical hypothesis and can be automated and repeated in a pipeline. A game day is a planned team exercise: a failure is injected and on-call responds with the real alerts and runbooks, so it tests people and process too, followed by a blameless review.