Observability and system security
Metrics, traces and logs, SLOs and error budgets, authentication, authorization and encryption.
On this page
Once a system is running, two questions never go away: is it working, and is it safe? The first half of this article is about seeing inside a system you cannot log into: the numbers, traces and logs that tell you what it is doing and the targets that say whether that is good enough. The second half is about who may do what, and how data stays private on the way through.
The map
Read this first when short on time. Every branch is a section below.
The three pillars in one picture
Metrics, traces and logs answer three different questions about the same request, and they only become observability when one id ties them together.
flowchart TD R["one request<br/>trace id 4bf9"] --> M["Metrics<br/>is something wrong, how much?<br/>p99 latency up 40%"] R --> T["Traces<br/>where is it slow?<br/>payment span took 380 ms"] R --> L["Logs<br/>what exactly happened?<br/>card declined, retry 2, trace 4bf9"] M -- alert fires --> T T -- click the slow span --> L
Metrics: numbers over time
A metric is a numeric value sampled over time and labelled with dimensions: requests per second by endpoint, error ratio by region, queue depth by topic. Metrics are cheap to store and fast to query, which makes them the layer for dashboards and alerts.
| Type | Meaning | Example | Query with |
|---|---|---|---|
| Counter | Only goes up; you read its rate | http_requests_total | rate(...[5m]) |
| Gauge | Current value, up or down | queue_depth, memory in use | Read directly, or max and min over a window |
| Histogram | Counts of observations in buckets, so percentiles can be computed across many instances | http_request_duration_seconds | histogram_quantile(0.99, ...) |
Percentiles, not averages
An average latency of 80 ms can hide a p99 of 3 seconds, and the p99 is what one in a hundred users feels. Report p50, p95 and p99. Tail latency also amplifies with fan-out: if a page calls 100 backends and each has a 1 percent chance of being slow, about 63 percent of page loads hit at least one slow call. Percentiles cannot be averaged across servers, which is why histograms with shared buckets, not pre-computed percentiles, are what you export.
Cardinality
Every unique combination of label values is a separate time series. A label with a thousand values multiplied by ten endpoints and five status codes is fifty thousand series, and a label like user_id is an outage of the metrics system itself. Keep labels to things with a bounded set of values; put the rest in traces and logs.
Which metrics
- RED for every service: Rate, Errors, Duration. If you have only three graphs per service, these are the three.
- USE for every resource: Utilisation, Saturation, Errors. CPU, memory, disks, connection pools, queues.
- The four golden signals are the same idea from the SRE book: latency, traffic, errors, saturation.
flowchart LR S1[service /metrics] -- scrape every 15 s --> P[(Prometheus TSDB)] S2[service /metrics] -- scrape --> P N[node exporter] -- scrape --> P P --> G[Grafana dashboards] P -- rules evaluated --> A[Alertmanager] A --> PD[on-call page] P -. downsample, long retention .-> LT[(Thanos or Mimir)]
# What a /metrics endpoint exposes (Prometheus text format)
http_requests_total{route="/orders",method="POST",status="200"} 18342
http_request_duration_seconds_bucket{route="/orders",le="0.1"} 17010
http_request_duration_seconds_bucket{route="/orders",le="0.5"} 18200
http_request_duration_seconds_bucket{route="/orders",le="+Inf"} 18342
# p99 latency per route over the last 5 minutes, across all instances
histogram_quantile(0.99, sum by (route, le) (rate(http_request_duration_seconds_bucket[5m])))
# error ratio: the SLI most services alert on
sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))
- Prometheus and Grafana are the default stack on Kubernetes; Prometheus descends from Google's internal Borgmon.
- CloudWatch is the push-based equivalent on AWS, with metrics from every managed service arriving without instrumentation.
- Datadog and Netflix Atlas operate at billions of series, which is where cardinality stops being a theory.
Tracing: following one request across services
A distributed trace records one request's path through every service it touched as a tree of timed spans. It is the tool that answers "which of the eleven services made this request slow", which no per-service metric can.
- Start a trace. The first service to see a request generates a trace id and a root span. Every operation worth timing (an HTTP call, a query, a queue publish) opens a child span with its own id and a parent id.
- Propagate the context. Outgoing calls carry the trace id and current span id in headers, so the next service continues the same trace instead of starting a new one. The W3C
traceparentheader is the standard; older systems used B3 headers. Messages on a queue carry the same fields in their metadata. - Export. Each service sends its finished spans to a collector, which batches them to a backend that stitches them into trees.
- Sample. Tracing every request at scale is expensive. Head-based sampling decides at the root (keep 1 percent) and is cheap but blind. Tail-based sampling buffers whole traces and keeps the interesting ones (errors, slow, rare paths), which is what you actually want and costs a buffering tier.
# One header carries the whole context between services
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
# | | | |
# version trace id (16 bytes) parent span id flags: 01 = sampled
- Google's Dapper paper (2010) is where the model comes from; Zipkin (Twitter) and Jaeger (Uber) are its open-source descendants.
- OpenTelemetry is now the single standard for instrumenting traces, metrics and logs, with auto-instrumentation for most HTTP clients, database drivers and frameworks; it feeds Jaeger, Tempo, Honeycomb, Datadog and AWS X-Ray alike.
- Service meshes emit spans from the sidecar for every call, which gives a trace of the network hops with no application changes; the application still has to propagate the headers.
A trace breaks the moment one service fails to forward the header, and the break is silent: you simply see two short traces instead of one long one. Async hops through queues and thread pools are where it usually happens; use the instrumentation library's context propagation rather than hand-rolled headers.
- Span
- One named, timed operation with a parent, attributes (route, status, database statement) and optional events.
- Context propagation
- Carrying the trace and span ids across a process or network boundary so the trace continues.
Logging: what exactly happened
A log is a timestamped record of a discrete event. Logs carry the detail that metrics aggregate away and traces summarise: the actual error message, the input that broke, the branch taken. They are also the most expensive of the three to store and search, so discipline matters.
flowchart TD A[app writes JSON to stdout] --> AG[agent on the node: Fluent Bit, Vector] AG -- batch, buffer, add pod and node labels --> ST[(store: OpenSearch, Loki, CloudWatch Logs)] ST --> UI[search and dashboards] ST -. after 14 days .-> CO[(cold storage: S3)] AG -. sample debug, drop noise .-> X[dropped]
- Structured. One JSON object per line with consistent field names, not free text. It is what makes logs searchable and lets the store index fields instead of words.
- Correlated. Every line carries the trace id and, where relevant, the request id, user id (or a pseudonym) and tenant. This is the link from Figure 2: click a span, see its logs.
- Levelled and sampled.
ERRORandWARNalways;INFOfor business events;DEBUGoff in production or sampled at 1 percent. A retry loop logging at full speed can cost more than the outage. - Centralised. Agents ship logs off the node so they survive the node and can be searched across the fleet. Retention tiers: hot for days, cold in object storage for months.
- Clean. No passwords, tokens, card numbers or full personal data. Logs are copied, exported and read by more people than any database.
{"ts":"2026-09-08T10:41:07.318Z","level":"WARN","service":"payment-service","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736",
"span_id":"00f067aa0ba902b7","event":"charge_declined","order_id":"o_81723","reason":"insufficient_funds","attempt":2,"duration_ms":241}
- The ELK stack (Elasticsearch, Logstash, Kibana) and its fork OpenSearch index every field and pay for it in storage; Grafana Loki indexes only labels and greps the rest, which is far cheaper for high volume.
- CloudWatch Logs Insights and Google Cloud Logging are the managed defaults; Datadog and Splunk are the commercial ones, priced by ingested gigabytes.
- Kubernetes made "write JSON to stdout, let the platform collect it" the universal convention.
Alerting on log lines ("more than 10 errors per minute") is fragile and slow; derive a metric from the event instead and alert on that. And a log line without a trace id is a fact you cannot connect to anything.
SLIs, SLOs and SLAs: deciding what good enough means
An SLI is a measurement of one aspect of service quality, an SLO is the target you hold yourself to on that measurement, and an SLA is the promise you make to customers with consequences attached. The three words are used loosely; the definitions are not.
- SLI
- Service level indicator. A ratio of good events to total events: successful requests / all requests, or requests faster than 300 ms / all requests.
- SLO
- Service level objective. A target for an SLI over a window: 99.9 percent of requests succeed over 30 days. Internal, and the number engineering plans around.
- SLA
- Service level agreement. A contract: if availability drops below 99.9 percent in a month, credits are paid. Always looser than the internal SLO so that the SLO breaks first.
- Error budget
- 1 minus the SLO, as a quantity of allowed failure: at 99.9 percent over 30 days, about 43 minutes of full downtime or the equivalent number of failed requests.
| Target | Downtime per year | Per 30 days | Per week | Feels like |
|---|---|---|---|---|
| 99 percent (two nines) | 3.65 days | 7.2 hours | 1.7 hours | Internal tools |
| 99.9 percent | 8.8 hours | 43 minutes | 10 minutes | Most web services |
| 99.95 percent | 4.4 hours | 22 minutes | 5 minutes | Typical cloud SLA for a single service |
| 99.99 percent | 53 minutes | 4.3 minutes | 1 minute | Payments, core infrastructure; needs automation to hit |
| 99.999 percent | 5.3 minutes | 26 seconds | 6 seconds | Telecom, storage; multi-region and no human in the loop |
Using the error budget
The budget turns reliability into a resource you spend on purpose. While there is budget left, ship fast, run risky migrations, experiment. When it is spent, releases pause and the team works on reliability until the window rolls the budget back. That policy, agreed in advance, is what stops the argument between "ship" and "stabilise" from being re-fought every week.
Alerting on burn rate
Alert on the SLI, not on causes. "CPU is at 90 percent" is not a user problem; "we are burning the 30-day error budget 14 times faster than sustainable" is. A burn rate of 1 means the budget lasts exactly the window. Standard practice pages on a high burn rate over a short window (14.4× over one hour: 2 percent of the monthly budget gone in an hour) and tickets on a low burn rate over a long window (1× over three days). Each alert also requires a shorter confirmation window so it resolves quickly once the problem stops.
flowchart TD
SLI["SLI: good requests ÷ all requests, 5 min windows"] --> SLO["SLO: 99.9% over 30 days"]
SLO --> EB["error budget: 0.1% of requests, about 43 min"]
EB --> BR{burn rate?}
BR -- 14.4x over 1 h --> PAGE[page on-call now]
BR -- 6x over 6 h --> PAGE
BR -- 1x over 3 days --> TICKET[ticket, fix this week]
EB -- budget spent --> FREEZE[pause releases, reliability work]
- Google's SRE books defined the vocabulary and the multi-window, multi-burn-rate alerting that most tooling now implements.
- AWS publishes SLAs per service: EC2 promises 99.99 percent per region with credits below that, and 99.5 percent for a single instance.
- Cloudflare, GitHub and Stripe publish status pages and post-incident reports that are, in effect, public error-budget accounting.
A 100 percent target is not a goal, it is a refusal to make trade-offs, and it makes every deploy a violation. Set the SLO at what users actually notice, and remember that a service's SLO cannot exceed the SLOs of what it depends on.
Authentication: proving who you are
Authentication establishes an identity: a user, a service, a device. Everything after it, including authorization, trusts that identity, so the mechanism has to be hard to forge and easy to revoke.
| Mechanism | How it works | Strengths | Weaknesses |
|---|---|---|---|
| Session cookie | Server stores session state; the browser sends an opaque id in an HttpOnly, Secure, SameSite cookie | Instantly revocable, small, browsers handle it | Needs a shared session store; CSRF must be handled |
| JWT | A signed token (header.payload.signature) carrying claims; the server verifies the signature and trusts the claims | Stateless, works across services, carries identity and roles | Cannot be revoked before expiry; must be short-lived (minutes) with a refresh token |
| API key | A long random secret per client, stored hashed on the server | Simple for machine clients and partners | Long-lived, easy to leak, no user identity |
| OAuth 2.0 + OIDC | Delegated login through an identity provider; the app receives an ID token (who) and an access token (what it may call) | No passwords in your app; SSO, MFA and passkeys handled by the provider | Several roles and flows to get right |
| Mutual TLS | Both sides present certificates; identity is the certificate | Strong service-to-service identity, no tokens to leak | Certificate issuance and rotation must be automated |
sequenceDiagram autonumber participant U as User's browser or app participant A as Your app participant IdP as Identity provider participant API as Your API U->>A: click "Sign in with Google" A-->>U: redirect to IdP with client_id, redirect_uri, scope, state, code_challenge U->>IdP: log in, MFA, consent IdP-->>U: redirect back with code and state U->>A: GET /callback?code=... A->>IdP: POST /token: code, client_secret, code_verifier IdP-->>A: ID token (JWT: who), access token, refresh token A-->>U: session cookie, or the tokens for a mobile app U->>API: request with Authorization: Bearer access token API->>API: verify signature and expiry with the IdP's public keys
- Sessions or tokens. For a browser talking to one backend, a session cookie is simpler and safer. For mobile apps and for calls between services, tokens. Many systems use both: a cookie at the edge, a short-lived JWT inside.
- Short access tokens, longer refresh tokens. An access token that lives 5 to 15 minutes limits the damage of a leak; a refresh token, stored more carefully and revocable server-side, mints new ones. Revocation lists for access tokens exist but reintroduce the state JWTs were meant to avoid.
- Validate at the gateway, trust inside. The API gateway verifies the token once and forwards a trusted identity header (or the token itself); services behind it verify again only if the network is not trusted, which is where mTLS and a service mesh come in.
- Service identity. Services authenticate to each other with mTLS certificates issued by an internal authority, or with workload identities from the platform (IAM roles, Kubernetes service accounts, SPIFFE ids). Never with a shared password in a config file.
// A decoded ID token: three base64url parts, signed by the provider's private key
// header: {"alg":"RS256","kid":"2024-key-1"}
// payload:
{
"iss": "https://accounts.example.com",
"sub": "user-8213",
"aud": "clio-web",
"exp": 1757328067,
"iat": 1757327167,
"email": "s@example.com",
"roles": ["editor"]
}
// signature: RS256(header + "." + payload) — verify with the key named by kid, then check iss, aud, exp
- Auth0, Okta, Amazon Cognito, Firebase Auth are hosted identity providers implementing OAuth 2.0 and OIDC; "Sign in with Google" and "Sign in with Apple" are the same flow with a consumer provider.
- AWS Signature Version 4 authenticates every AWS API call by signing the request with the caller's secret key, which is the API key idea done properly: the secret never travels.
- Istio and Linkerd issue short-lived mTLS certificates to every workload automatically, using SPIFFE identities.
- Passkeys (WebAuthn) are replacing passwords with device-bound key pairs; the identity providers above already support them.
The classic JWT bugs: accepting alg: none or letting the token choose the algorithm; not checking aud so a token for another app is accepted; hour-long access tokens with no refresh; and putting a JWT in localStorage where any injected script can read it. Verify with a well-maintained library, pin the algorithm, and check issuer, audience and expiry every time.
Authorization: deciding what you may do
Authorization takes an authenticated identity and a requested action on a resource and answers yes or no. The model you choose decides how policies are expressed; where you enforce it decides whether the answer can be bypassed.
flowchart LR REQ["request: user 8213 wants to DELETE document 42"] --> PEP[enforcement point in the service] PEP -- "who, what, which, context" --> PDP[decision point: policy engine] POL[(policies: RBAC rules, ABAC conditions, relationship tuples)] --> PDP PDP -- allow or deny --> PEP PEP -- allow --> DO[perform the action, write an audit log] PEP -- deny --> NO[403, and audit that too]
| Model | Policies look like | Good for | Breaks down when | Examples |
|---|---|---|---|---|
| RBAC | Users have roles; roles have permissions | Organisations with a handful of job functions | Roles multiply to express exceptions (role explosion) | Kubernetes RBAC, most SaaS admin panels |
| ABAC | Rules over attributes of the subject, resource and environment: allow if doc.owner == user.id and time in business hours | Fine-grained, contextual rules | Policies become hard to audit; answering "who can access X" is expensive | AWS IAM conditions, OPA, Cedar |
| ReBAC | Relationships as tuples: doc:42#editor@user:8213, with inheritance (folder editors edit documents) | Sharing models: documents, folders, groups, orgs | Needs a purpose-built store to answer checks fast at scale | Google Zanzibar, SpiceDB, Auth0 FGA, OpenFGA |
- Least privilege and deny by default. Nothing is allowed until a policy says so, and identities get the minimum they need. This applies to service accounts as much as people.
- Check at the resource, not just the route. "Is the user logged in" at the gateway is not authorization. The service must check that this user may act on this object. Skipping it is the most common API vulnerability (broken object-level authorization: change the id in the URL and read someone else's order).
- Centralise the policy, not the check. Policies live in one engine so they can be reviewed and tested; the check runs inside each service at the point of action.
- Audit. Every allow and deny on sensitive resources is logged with who, what and why. This is what makes incident review possible.
// AWS IAM: the policy language most engineers meet first. ABAC via Condition.
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::clio-uploads/${aws:userid}/*",
"Condition": { "Bool": { "aws:SecureTransport": "true" } }
}
- Google Zanzibar answers authorization checks for Drive, YouTube and Calendar at millions of checks per second with relationship tuples; SpiceDB and OpenFGA are open-source versions.
- Open Policy Agent evaluates Rego policies as a sidecar or library and is used for Kubernetes admission control and service authorization at Netflix and Pinterest.
- AWS IAM is ABAC at cloud scale: identity policies, resource policies and conditions evaluated on every API call, deny by default.
- GitHub is a visible ReBAC system: organisation, team and repository roles that inherit.
Encryption: in transit, at rest, and the keys
Encryption in transit stops anyone on the network from reading or altering traffic; encryption at rest stops anyone with the disk from reading the data. Both are only as strong as the handling of the keys, which is why key management is the actual engineering problem.
In transit: TLS
sequenceDiagram autonumber participant C as Client participant S as Server C->>S: ClientHello: supported ciphers, key share, SNI S-->>C: ServerHello: chosen cipher, key share, certificate, Finished Note over C: verify the certificate chain to a trusted CA, derive the shared key C->>S: Finished, then encrypted application data Note over C,S: TLS 1.3 needs one round trip. A resumed session can send data with its first flight (0-RTT)
- Certificates bind a name to a public key and are signed by a certificate authority the client already trusts. Automation (ACME with Let's Encrypt, cloud certificate managers, cert-manager on Kubernetes) issues and rotates them; expired certificates are a top cause of self-inflicted outages.
- Where it ends. TLS is terminated at the edge (the networking article covers why) and re-established inside with mTLS or a mesh when the internal network is not trusted, which under a zero-trust policy is always.
- Versions. TLS 1.3 only, or 1.2 with modern ciphers where legacy clients demand it. Nothing older.
At rest: envelope encryption
flowchart LR
KMS[("KMS / HSM<br/>master key never leaves")] -- generate data key --> APP[application]
APP -- "encrypt data with the plaintext data key (AES-256-GCM)" --> D[(ciphertext)]
APP -- "store the encrypted data key next to the data" --> D
D -. "to read: send encrypted data key to KMS, get plaintext key, decrypt" .-> KMS
- Symmetric for data. AES-256-GCM (authenticated: tampering is detected) encrypts the bytes. A fresh data key per object or per tenant limits the blast radius of a leak.
- A key management service holds the master keys. They are generated inside a hardware module and never exported; the service encrypts and decrypts data keys on request, logs every use, and enforces IAM on who may ask.
- Rotation. Master keys rotate on a schedule; because data keys are what encrypt data, rotation is cheap. Data keys rotate by re-encrypting, usually lazily.
- Server-side or client-side. Server-side (S3 SSE, database TDE) protects against stolen disks and is transparent. Client-side encryption, where the application encrypts before sending, protects against the storage provider too, at the cost of losing server-side search and indexing.
Secrets and passwords
- Secrets (database passwords, API keys, private keys) live in a secrets manager, are injected at runtime, rotate automatically where possible, and never appear in source control, images or logs. Prefer short-lived credentials (IAM roles, workload identity) to long-lived secrets wherever a platform offers them.
- Passwords are hashed, never encrypted. A slow, salted hash (Argon2id, bcrypt, scrypt) so that a stolen table cannot be reversed at scale. Encryption implies a key that decrypts everything; hashing has no way back.
- AWS KMS, Google Cloud KMS and Azure Key Vault are the managed key services; S3, EBS, RDS and DynamoDB all use envelope encryption with keys from them by default.
- HashiCorp Vault and AWS Secrets Manager store and rotate secrets; Vault can also issue short-lived database credentials per service.
- Let's Encrypt issues a large share of the web's certificates for free through ACME, which is why 90-day certificates and automatic renewal became normal.
- Signal and WhatsApp are end-to-end encryption: the server stores ciphertext it cannot read, which is client-side encryption taken to its conclusion.
Encryption at rest protects against stolen disks, not against a compromised application: the app can decrypt, so an attacker inside the app can too. Access control and audit logs are what protect data from that. And never implement cryptographic primitives yourself; use the platform's libraries with their defaults.
Before any of this, traffic passes a WAF that blocks known attack patterns (injection, cross-site scripting) and DDoS protection that absorbs floods at the network layer, both usually provided by the CDN or cloud (Cloudflare, AWS Shield and WAF, Google Cloud Armor). They are the outer ring of defence in depth; authentication, authorization and encryption are the inner ones.
Recap
- Metrics say whether something is wrong, traces say where, logs say what; a trace id threads all three.
- Counters, gauges and histograms; report percentiles, export histograms not averages, keep label cardinality bounded, and start every service with RED.
- A trace is a tree of spans joined by propagated context (the
traceparentheader); sample at the tail to keep the interesting traces; OpenTelemetry is the standard. - Logs are structured JSON with a trace id, shipped by an agent, levelled and sampled, and never contain secrets; alert on metrics derived from them, not on the lines.
- SLI is the measurement, SLO the target, SLA the contract. The error budget is the SLO's complement and the policy for spending it settles ship-versus-stabilise. Alert on burn rate over multiple windows.
- Authentication: sessions for browsers, short-lived JWTs with refresh tokens for apps and services, OAuth 2.0 with PKCE and OIDC for delegated login, mTLS or workload identity between services. Validate signature, issuer, audience, expiry.
- Authorization: RBAC for simple roles, ABAC for contextual rules, ReBAC for sharing models. Deny by default, least privilege, check at the object in the service, audit everything sensitive.
- TLS 1.3 in transit with automated certificates; envelope encryption at rest with master keys in a KMS; secrets in a secrets manager; passwords hashed with Argon2id or bcrypt.
- WAF and DDoS protection at the edge are the outer ring; encryption at rest does not protect against a compromised application.
Questions
Try answering each one out loud before opening it. Lead with the one-line answer, then the reasoning, then one detail from a real system.
What are the three pillars of observability, and how do they relate to each other?
Metrics, traces and logs: metrics show that something is wrong and how much, traces show where in the request path, logs show exactly what happened; they become observability when a trace id links them so you can move from an alert to a span to a log line.
- Metrics are cheap aggregates for dashboards and alerts.
- Traces are per-request trees of timed spans across services.
- Logs are the detailed events, the most expensive to store.
OpenTelemetry instruments all three with one SDK and one context, which is why the trace id ends up in metrics exemplars and log lines.
Why report p99 latency instead of the average?
Because the average hides the tail: a mean of 80 ms is compatible with one in a hundred users waiting three seconds, and with fan-out those slow calls hit a large share of page loads.
- With 100 backends each 1 percent slow, about 63 percent of requests hit at least one slow call.
- Percentiles cannot be averaged across servers, so export histograms with shared buckets and compute the quantile at query time.
- Track p50, p95 and p99 together; the gap between them is a diagnosis in itself.
Prometheus's histogram_quantile over summed buckets is the standard way to get a fleet-wide p99.
What is the cardinality problem in metrics?
Every distinct combination of label values is its own time series, so a label with many values multiplies series counts until the metrics store runs out of memory or money; high-cardinality identifiers belong in traces and logs, not metric labels.
- Ten endpoints times five statuses times a thousand customers is fifty thousand series for one metric.
- User ids, request ids and raw URLs are the usual offenders.
- Bound labels to enumerations: route template, method, status class, region.
Datadog and Prometheus both price or fail on series count, which is why teams learn this the hard way.
Which metrics would you put on the first dashboard for a new service?
RED for the service, rate, errors and duration as p50, p95 and p99, plus USE for its resources: utilisation, saturation and errors of CPU, memory, connection pools and queues.
- RED is what users experience; USE is what will break next.
- Google's four golden signals (latency, traffic, errors, saturation) are the same set.
- Add the dependency's RED as seen from this service, since most incidents are downstream.
If the alerting is on the SLI, the dashboard exists to explain the alert, not to be watched.
How does distributed tracing work end to end?
The first service creates a trace id and root span, every operation opens a child span, outgoing calls carry the trace and span ids in a traceparent header so the next service continues the tree, spans are exported to a collector, and a backend stitches them into a waterfall.
- Context propagation is the fragile part: one service that drops the header splits the trace.
- Queues carry the context in message metadata; thread pools need the library's context handling.
- Head-based sampling is cheap and blind; tail-based keeps errors and slow traces.
Dapper at Google defined the model; OpenTelemetry with Jaeger, Tempo or X-Ray is the modern stack.
What makes a log line useful, and what should never be in one?
Structure (one JSON object with consistent fields), correlation (trace id, request id, tenant), a level, and a named event; never passwords, tokens, card numbers or full personal data.
- Structured fields make logs queryable and let the store index only what matters.
- Write to stdout and let an agent ship, buffer and enrich.
- Sample debug logs and cap retry-loop logging; volume is the cost driver.
Loki's design, indexing labels only and grepping the body, is a direct response to how expensive full-text indexing of logs became.
Explain SLI, SLO and SLA, and what an error budget is for.
An SLI is a measurement of good events over total events, an SLO is the internal target for that SLI over a window, an SLA is the external contract with penalties and is always looser than the SLO; the error budget is the allowed failure implied by the SLO and is spent deliberately on velocity.
- 99.9 percent over 30 days is roughly 43 minutes of downtime or the equivalent failed requests.
- Budget remaining means ship; budget spent means releases pause and reliability work starts.
- A service cannot promise more than its dependencies deliver.
The policy is agreed in advance so the ship-versus-stabilise argument is settled by a number, not a meeting.
How should you alert on an SLO?
On burn rate: how fast the error budget is being consumed relative to the window, using several window lengths so fast burns page immediately and slow burns create tickets, with a short confirmation window so alerts resolve quickly.
- Page at 14.4× over one hour (2 percent of the monthly budget in an hour) and 6× over six hours.
- Ticket at 1× over three days.
- Alert on symptoms users feel, not causes such as CPU.
These defaults come from Google's SRE workbook and most SLO tools implement them as-is.
What does 99.99 percent availability mean in practice?
About 53 minutes of downtime a year, 4 minutes a month and 1 minute a week, which means no human can be in the recovery loop: detection, failover and rollback must be automatic.
- Three nines is 43 minutes a month and is where most web services sit.
- Five nines is 26 seconds a month and implies multi-region active-active with no manual steps.
- Each extra nine roughly multiplies cost and complexity, so pick the one users notice.
AWS's EC2 regional SLA is 99.99 percent, and reaching it as a customer means spreading across availability zones yourself.
Sessions or JWTs: how do you choose, and how do you revoke a JWT?
Sessions for browsers talking to one backend because they are simple and instantly revocable; JWTs for mobile apps and service-to-service calls because they are stateless and carry identity across services; a JWT cannot be revoked before expiry, so keep it short-lived and revoke the refresh token instead.
- Access token lifetime of 5 to 15 minutes bounds the damage of a leak.
- A server-side denylist works for emergencies but reintroduces state.
- Store browser tokens in HttpOnly cookies, not localStorage.
Many systems combine both: a session cookie at the edge and a short JWT minted for internal calls.
What is the difference between OAuth 2.0 and OpenID Connect, and why use the authorization code flow with PKCE?
OAuth 2.0 is a delegation protocol for obtaining an access token to call an API on a user's behalf; OpenID Connect adds an identity layer on top, the ID token, so the app also learns who the user is; the authorization code flow with PKCE keeps tokens out of the browser URL and proves the same client that started the login is finishing it.
- Roles: resource owner (user), client (your app), authorization server (the identity provider), resource server (your API).
- The implicit flow returned tokens in the URL fragment and is deprecated.
- PKCE replaced the client secret for public clients such as mobile apps and single-page apps.
"Sign in with Google" is OIDC; a "connect your calendar" permission screen is OAuth scopes.
How do services authenticate to each other?
With mutual TLS, where each service presents a certificate issued by an internal authority and the certificate is its identity, or with platform workload identities such as IAM roles and Kubernetes service accounts; never with shared static secrets.
- A service mesh issues and rotates short-lived certificates automatically.
- SPIFFE gives each workload a standard identity that the certificates encode.
- Short-lived credentials from the platform beat long-lived API keys in config files.
Istio turns on mTLS for every pod-to-pod call with one setting, which is the most common way teams get there.
RBAC, ABAC or ReBAC: when does each fit, and where do you enforce it?
RBAC for a small set of job roles, ABAC for contextual rules over attributes, ReBAC for sharing models built on relationships; enforce in the service at the point of action on the specific object, with policy decisions centralised in an engine.
- RBAC fails through role explosion; ABAC through unauditable policies; ReBAC needs a dedicated store to answer checks fast.
- Deny by default and least privilege apply to every model.
- Audit every sensitive allow and deny.
Google Zanzibar serves Drive's sharing checks with relationship tuples, and OpenFGA brings the same model to everyone else.
What is broken object-level authorization, and how do you prevent it?
It is checking that a user is logged in but not that they may act on the specific object requested, so changing an id in the URL exposes someone else's data; prevent it by checking ownership or permission on every object access inside the service.
- It has topped the OWASP API security list because it is easy to miss and easy to exploit.
- Gateway authentication does not prevent it; only a check against the resource does.
- Scope database queries by the caller's identity rather than filtering after loading.
Non-guessable ids help slightly, but they are obscurity, not authorization.
Walk through a TLS 1.3 handshake and say where you would terminate it.
The client sends supported ciphers and a key share, the server replies with its choice, its own key share and certificate, the client verifies the certificate chain and both derive the session key, all in one round trip; terminate at the edge for latency and certificate management, then re-encrypt inside with mTLS where the network is not trusted.
- Ephemeral key exchange gives forward secrecy.
- Resumed sessions can send data in the first flight (0-RTT).
- Automated certificate issuance (ACME, cloud certificate managers) prevents the expiry outages.
Cloud load balancers terminate TLS with managed certificates and can re-encrypt to backends in one setting.
Explain envelope encryption and why it makes key rotation cheap.
Data is encrypted with a per-object data key, and the data key is encrypted with a master key that lives in a key management service and never leaves it; rotating the master key only re-encrypts the small data keys, never the data.
- The KMS logs and authorises every decrypt of a data key.
- Per-tenant or per-object data keys limit the blast radius of a leak.
- Server-side encryption protects against stolen disks; client-side protects against the storage provider too.
S3, EBS and RDS all use exactly this scheme with KMS keys, which is why enabling encryption there costs nothing noticeable.
How should secrets and passwords be handled?
Secrets live in a secrets manager, are injected at runtime, rotate automatically, and never sit in repositories, images or logs, with short-lived platform credentials preferred over long-lived keys; passwords are hashed with a slow salted algorithm such as Argon2id or bcrypt, never encrypted.
- Encryption implies a key that decrypts everything; a hash has no way back.
- Vault can issue per-service database credentials that expire.
- Scan repositories and images for leaked secrets as part of CI.
Most real breaches involve a long-lived credential in a place it should not have been, not a broken cipher.
Design authentication and authorization for a mobile app talking to a set of microservices.
Log in through an identity provider with the authorization code flow plus PKCE, hold a short-lived access token and a refresh token in secure device storage, validate the token at the API gateway and forward identity inward, run mTLS between services, and have each service check object-level permissions against a central policy before acting.
- Gateway: signature, issuer, audience, expiry, then rate limits per user.
- Services: trust the gateway's identity header only over mTLS; check permissions per resource; audit sensitive actions.
- Refresh tokens are revocable server-side, which is how logout and compromise are handled.
This is the shape Cognito or Auth0 plus an API gateway plus a service mesh gives you with little custom code.