System design · 5 of 5

Observability and system security

Metrics, traces and logs, SLOs and error budgets, authentication, authorization and encryption.

Updated 2026-09-08
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.

Figure 1. The whole article on one page. Every branch is a section below; fold what you know, open what you do not.

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
Figure 2. Metrics tell you to look, traces tell you where, logs tell you what. The trace id is the thread through all three.

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.

TypeMeaningExampleQuery with
CounterOnly goes up; you read its ratehttp_requests_totalrate(...[5m])
GaugeCurrent value, up or downqueue_depth, memory in useRead directly, or max and min over a window
HistogramCounts of observations in buckets, so percentiles can be computed across many instanceshttp_request_duration_secondshistogram_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

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)]
Figure 3. The pull model: services expose a page of numbers and the collector fetches it. Push models (CloudWatch, StatsD) send instead.
# 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]))
In the wild
  • 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.

trace 4bf92f3577b34da6a3ce929d0e0e4736 · 420 ms total · each bar is a span 0 ms420 ms api-gateway 420 ms order-service db: SELECT cart 60 ms payment-service stripe: POST /charges 240 ms: this is the slow one cache: SET order 7 ms
Figure 4. A trace as a waterfall. Nesting shows who called whom; width shows where the time went.
  1. 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.
  2. 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 traceparent header is the standard; older systems used B3 headers. Messages on a queue carry the same fields in their metadata.
  3. Export. Each service sends its finished spans to a collector, which batches them to a backend that stitches them into trees.
  4. 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
In the wild
  • 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.
Watch out

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]
Figure 5. Write to stdout, let an agent ship it. The app never knows where its logs go.
{"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}
In the wild
  • 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.
Watch out

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.
TargetDowntime per yearPer 30 daysPer weekFeels like
99 percent (two nines)3.65 days7.2 hours1.7 hoursInternal tools
99.9 percent8.8 hours43 minutes10 minutesMost web services
99.95 percent4.4 hours22 minutes5 minutesTypical cloud SLA for a single service
99.99 percent53 minutes4.3 minutes1 minutePayments, core infrastructure; needs automation to hit
99.999 percent5.3 minutes26 seconds6 secondsTelecom, 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]
Figure 6. From a measurement to a decision. The numbers are the SRE book's defaults and most teams keep them.
In the wild
  • 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.
Watch out

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.

MechanismHow it worksStrengthsWeaknesses
Session cookieServer stores session state; the browser sends an opaque id in an HttpOnly, Secure, SameSite cookieInstantly revocable, small, browsers handle itNeeds a shared session store; CSRF must be handled
JWTA signed token (header.payload.signature) carrying claims; the server verifies the signature and trusts the claimsStateless, works across services, carries identity and rolesCannot be revoked before expiry; must be short-lived (minutes) with a refresh token
API keyA long random secret per client, stored hashed on the serverSimple for machine clients and partnersLong-lived, easy to leak, no user identity
OAuth 2.0 + OIDCDelegated 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 providerSeveral roles and flows to get right
Mutual TLSBoth sides present certificates; identity is the certificateStrong service-to-service identity, no tokens to leakCertificate 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
Figure 7. The OAuth 2.0 authorization code flow with PKCE, which OpenID Connect builds on. The app never sees the user's password.
// 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
In the wild
  • 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.
Watch out

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]
Figure 8. Separate deciding from enforcing. The decision point can be a library, a sidecar or a service; the enforcement point is always in the code path.
ModelPolicies look likeGood forBreaks down whenExamples
RBACUsers have roles; roles have permissionsOrganisations with a handful of job functionsRoles multiply to express exceptions (role explosion)Kubernetes RBAC, most SaaS admin panels
ABACRules over attributes of the subject, resource and environment: allow if doc.owner == user.id and time in business hoursFine-grained, contextual rulesPolicies become hard to audit; answering "who can access X" is expensiveAWS IAM conditions, OPA, Cedar
ReBACRelationships as tuples: doc:42#editor@user:8213, with inheritance (folder editors edit documents)Sharing models: documents, folders, groups, orgsNeeds a purpose-built store to answer checks fast at scaleGoogle Zanzibar, SpiceDB, Auth0 FGA, OpenFGA
// 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" } }
}
In the wild
  • 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)
Figure 9. TLS 1.3. The key exchange gives forward secrecy: a stolen server key does not decrypt past sessions.

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
Figure 10. Envelope encryption. The master key encrypts many data keys; rotating it never requires re-encrypting the data.

Secrets and passwords

In the wild
  • 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.
Watch out

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.

The edge layers

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 traceparent header); 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.