DevOps ·

SLIs, SLOs, and SLAs in Practice: Designing Reliability Targets for Startup Engineering Teams

SLIs measure the thing, SLOs set the target, SLAs are the business contract. This covers how to choose the right SLIs for different service types, set achievable SLO targets, implement error budget policies, instrument SLIs in TypeScript, and build burn-rate alerts that actually fire at the right time.

SLIs, SLOs, and SLAs in Practice: Designing Reliability Targets for Startup Engineering Teams

Most startups operate without formal reliability targets. They know when things are broken because customers tell them. That works early. It stops working when you have paying customers with uptime expectations, a team larger than three engineers, or any service dependency that needs a number in a contract.

SLIs, SLOs, and SLAs give you a shared language for reliability and a policy for when to stop shipping features and fix infrastructure. This is the practical version: how to instrument the measurements, set targets you will actually hit, and use the framework without drowning in dashboards.

The Three-Layer Model

These three terms are consistently misunderstood or conflated. The relationships between them matter.

SLI (Service Level Indicator): A specific, measurable signal about how your service is behaving. It is a ratio or quantile over a time window. Examples: the fraction of HTTP requests returning 2xx over the last 28 days, the 99th percentile latency for your checkout API over the last hour, the fraction of background jobs completing within 60 seconds.

SLO (Service Level Objective): A target range applied to an SLI. “99.5% of requests over a rolling 28-day window should return 2xx.” The SLO is an internal engineering commitment. It is the thing your team decides to defend.

SLA (Service Level Agreement): A contractual commitment to customers, often with financial penalties for violations. SLAs are almost always less strict than your internal SLOs. If your SLO is 99.5% availability, your SLA might be 99%. The gap between them is your buffer. When your SLO is at risk, you have warning before you have breached a customer contract.

A common mistake: teams write SLAs without SLOs, then discover they have no internal signal that they are approaching a contract violation until they are already past it.

Choosing the Right SLIs

Not every metric is a useful SLI. The signal needs to reflect something the user actually experiences. If users cannot feel it, it is not an SLI.

API Services

For synchronous APIs, the canonical SLIs are:

  • Availability: count(2xx or 3xx responses) / count(all responses) over a rolling window
  • Latency: count(requests completing under threshold) / count(all requests). Use a percentile framing rather than average. P99 matters. P50 is almost always fine.

Percentile framing is worth explaining. Raw average latency hides the outliers that users actually experience. A better SLI is: “What fraction of requests completed in under 300ms?” This converts the latency distribution into a ratio, which makes burn rate calculations easier and more meaningful.

Background Jobs and Queues

Queue-based services need different SLIs:

  • Freshness: Time elapsed since the last successful job completion. If a data sync should run every 5 minutes and it has not completed in 20 minutes, that is a freshness violation.
  • Throughput: count(jobs completing successfully) / count(jobs attempted) over a window
  • Completion latency: Fraction of jobs completing within an agreed time budget (e.g., 95% of emails sent within 30 seconds of being enqueued)

Data Pipelines

For pipelines that transform or aggregate data, correctness is often the right SLI:

  • Correctness: count(records passing validation) / count(records processed). Define what “correct” means precisely. This might be schema conformance, referential integrity, or a business rule.

The wrong instinct is to use infrastructure metrics (CPU, memory, queue depth) as SLIs. Those are useful for alerting on impending problems, but they do not measure what users experience. Keep SLIs user-facing.

Setting SLO Targets

The most common mistake is setting targets too high. A 99.9% availability SLO sounds conservative, but it only allows 43 minutes of downtime per month. If your deployment pipeline takes 10 minutes and you deploy twice a week, you have almost no budget left for actual failures.

A practical starting point for most SaaS products:

Service TypeStarting SLOMonthly Error Budget
Core API (user-facing)99.5% availability~3.6 hours
API latency P99 < 500ms95% of requests5% headroom
Background jobs success rate99%~7 hours equivalent
Email/notification delivery99.5% within 60s~3.6 hours
Data pipeline correctness99.9%~43 minutes

Start looser than you think you need. You can tighten targets after you understand your actual error rate. An SLO that you always hit by a wide margin is either measuring the wrong thing or set too loose. An SLO you can never hit is demoralizing and gets ignored.

The other axis is the measurement window. Rolling 28-day windows are better than calendar months for two reasons: they are continuous (no cliff at month boundaries) and they make burn rate math straightforward. A 30-day window with a 99.5% SLO means you have 0.5% of 30 days = 3.6 hours of error budget per month.

Instrumenting SLIs in TypeScript

The instrumentation pattern that works at startup scale: record SLI events explicitly in your application layer, rather than inferring them from raw HTTP logs. This gives you control over what counts as a valid request and what counts as a failure.

// sli-recorder.ts
export interface SLIEvent {
  service: string;
  operation: string;
  success: boolean;
  latencyMs: number;
  timestamp: number;
}

export class SLIRecorder {
  private readonly service: string;

  constructor(service: string) {
    this.service = service;
  }

  async record<T>(
    operation: string,
    fn: () => Promise<T>
  ): Promise<T> {
    const start = Date.now();
    let success = false;
    try {
      const result = await fn();
      success = true;
      return result;
    } catch (err) {
      success = false;
      throw err;
    } finally {
      const latencyMs = Date.now() - start;
      this.emit({
        service: this.service,
        operation,
        success,
        latencyMs,
        timestamp: Date.now(),
      });
    }
  }

  private emit(event: SLIEvent): void {
    // Ship to your metrics backend
    // Prometheus: increment counters
    // Datadog: statsd client
    // Custom: POST to aggregation endpoint
    metrics.increment(`sli.${event.service}.${event.operation}`, {
      success: String(event.success),
    });
    metrics.histogram(
      `sli.${event.service}.${event.operation}.latency`,
      event.latencyMs
    );
  }
}

At the route level, wrap handlers with the recorder:

// api/checkout.ts
const sli = new SLIRecorder("checkout-api");

export async function handleCheckout(req: Request): Promise<Response> {
  return sli.record("process_order", async () => {
    const order = await validateOrder(req);
    const payment = await chargeCard(order);
    await fulfillOrder(payment);
    return Response.json({ orderId: payment.orderId });
  });
}

The key design decision: what counts as a failure? An unhandled exception should always be a failure. But some 4xx errors (bad input from the user) should not count against your SLI. A 400 because the user sent invalid JSON is not a reliability failure.

export class SLIRecorder {
  async record<T>(
    operation: string,
    fn: () => Promise<T>,
    options: { excludeClientErrors?: boolean } = {}
  ): Promise<T> {
    const start = Date.now();
    let success = false;
    try {
      const result = await fn();
      success = true;
      return result;
    } catch (err) {
      // 4xx errors are client mistakes, not reliability failures
      const isClientError =
        options.excludeClientErrors &&
        err instanceof HttpError &&
        err.status >= 400 &&
        err.status < 500;
      success = isClientError ? true : false;
      throw err;
    } finally {
      this.emit({ service: this.service, operation, success, latencyMs: Date.now() - start, timestamp: Date.now() });
    }
  }
}

For latency SLIs, use a threshold-based counter rather than raw histograms:

private emit(event: SLIEvent): void {
  const LATENCY_THRESHOLD_MS = 300;

  metrics.increment(`sli.requests.total`, {
    service: event.service,
    operation: event.operation,
  });

  if (event.success) {
    metrics.increment(`sli.requests.good`, {
      service: event.service,
      operation: event.operation,
    });
  }

  // Latency SLI: fraction under threshold
  if (event.latencyMs < LATENCY_THRESHOLD_MS) {
    metrics.increment(`sli.latency.good`, {
      service: event.service,
      operation: event.operation,
    });
  }

  metrics.increment(`sli.latency.total`, {
    service: event.service,
    operation: event.operation,
  });
}

This gives you two ratios you can query directly: sli.requests.good / sli.requests.total for availability and sli.latency.good / sli.latency.total for latency SLI. Both are computable over any time window your metrics backend supports.

Error Budget Policies

An error budget is (1 - SLO) * window_duration. For a 99.5% SLO over 28 days, the budget is 0.5% of 28 days = 3.36 hours.

The budget is only useful if you have a written policy for what happens when it gets consumed. Without a policy, teams watch the budget drain without changing behavior.

A practical policy has three thresholds:

Budget consumed < 50% (Green): Normal operations. Ship features, run experiments, deploy as usual.

Budget consumed 50-90% (Yellow): Slow down risky changes. Defer non-critical deployments. Review any incidents from the current window. This is a signal to investigate, not to stop shipping.

Budget consumed > 90% (Red): Feature freeze on the affected service. Engineering focus shifts to reliability work until budget recovers. No new experiments, no risky migrations.

The key is that the policy is written down before you hit red, agreed to by engineering and product, and enforced. The moment the policy depends on a judgment call during an incident is the moment it will not be applied consistently.

Burn Rate Alerts

Threshold alerts (fire when the error rate exceeds 1%) are the wrong model for SLOs. If your SLO window is 28 days and your error rate spikes to 2% for 10 minutes at 3am, a threshold alert fires but your error budget barely moves. You get woken up for something that will not cause an SLO violation.

Burn rate alerts measure how fast you are consuming budget relative to the rate that would exhaust it in one window. A burn rate of 1x exhausts the budget in exactly 28 days. A burn rate of 14.4x exhausts it in 2 days.

The Google SRE workbook recommends a two-window approach to reduce alert noise:

// burn-rate.ts
export function calculateBurnRate(
  errorRate: number,       // fraction: 0.01 = 1%
  sloTarget: number,       // fraction: 0.995 = 99.5%
): number {
  const errorBudget = 1 - sloTarget;  // 0.005
  return errorRate / errorBudget;     // burn rate multiplier
}

// Example: 2% error rate against a 99.5% SLO
// errorBudget = 0.005
// burnRate = 0.02 / 0.005 = 4x
// You will exhaust the monthly budget in 28 / 4 = 7 days

export interface BurnRateAlert {
  name: string;
  shortWindowHours: number;
  longWindowHours: number;
  burnRateThreshold: number;
  severity: "page" | "ticket";
}

export const BURN_RATE_ALERTS: BurnRateAlert[] = [
  {
    name: "critical",
    shortWindowHours: 1,
    longWindowHours: 5,
    burnRateThreshold: 14.4,  // Exhausts budget in 2 days
    severity: "page",
  },
  {
    name: "high",
    shortWindowHours: 6,
    longWindowHours: 30,
    burnRateThreshold: 6,     // Exhausts budget in ~4.7 days
    severity: "page",
  },
  {
    name: "medium",
    shortWindowHours: 120,
    longWindowHours: 720,
    burnRateThreshold: 3,     // Exhausts budget in ~9 days
    severity: "ticket",
  },
];

The two-window check (both short and long window must exceed the threshold) is load-bearing for alert quality. A single window check fires during transient spikes. Requiring both windows to exceed the threshold means the condition must be sustained, not a 30-second blip.

SLO Dashboards

A minimal SLO dashboard for a startup needs four panels per service:

  1. Current SLI over the last hour (are we good right now?)
  2. SLI trend over the rolling 28-day window (are we on track?)
  3. Error budget remaining in hours and as a percentage
  4. Burn rate over the last 6 hours

Start with these four. The current SLI and burn rate tell you what is happening now. The trend and budget remaining tell you whether it is an anomaly or a pattern.

The Organizational Side

SLOs fail organizationally more often than technically. The instrumentation is straightforward. Getting the team to actually use them is not.

Ownership: Each SLO should have a named owner. Not a team, a person. That person is responsible for writing the SLO, keeping the instrumentation accurate, and initiating the error budget policy when thresholds are crossed. Teams tend to diffuse responsibility. Named owners tend not to.

Buy-in process: Start with one service and one SLI. Measure for 30 days before setting any target. Set the initial target slightly below your measured baseline (if your actual availability is 99.6%, set the SLO at 99.4%). A target you will definitely hit builds confidence in the process before the policy has any teeth.

Review cadence: Monthly is right. Quarterly misses trends. Weekly is overhead. In the review, ask: Did we meet the SLO? Did the budget policy trigger? Are SLI definitions still measuring what users experience?

SLOs and incident retrospectives: Every significant incident should update at least one SLO. Either tighten the target to catch this failure earlier, or fix the SLI definition. If an incident happened and no SLO was violated, the SLO is set too loose.

Common Mistakes

Setting SLOs too tight from the start. 99.99% availability is 4.4 minutes of downtime per month. For most startups, that number is not grounded in any measurement. Teams set it because it “sounds good,” then watch the budget drain on every deployment. Start loose and tighten based on actual data.

Measuring the wrong thing. Tracking uptime of the server process is not the same as tracking whether users can complete their core workflow. A service can be up and still be functionally broken. SLIs should trace the path a user actually takes, not the infrastructure supporting it.

No consequences for violations. An SLO without a policy is a metric nobody acts on. If the error budget is exhausted and nothing changes, teams learn to ignore the number. The policy (feature freeze, reliability sprint, post-mortem required) is what gives the SLO meaning.

Using averages instead of ratios. Average latency is almost useless as an SLI. A long-running batch job completing in 60 seconds can pull the average up while interactive requests are fast. Ratios (fraction of requests under threshold) reflect user experience accurately. Averages do not.

Skipping the definition document. Every SLO needs a written definition: what counts as a good event, what counts as a bad event, the measurement window, and what traffic is excluded. Without it, the SLI drifts as the codebase changes. The metric you set today is not the metric you are running six months from now.

Production Considerations

Window boundaries and cold start. Rolling windows mean new services start with no data. Do not enforce error budget policies until you have one full window of measurement. Write this into the policy explicitly.

SLI exclusions. Planned maintenance, upstream dependency outages, and known-bad traffic (load tests) should be documented exclusions from SLI calculation. Without documented exclusions, teams argue about whether violations count, which erodes the policy.

Aggregation granularity. Record SLI events at the request level. Aggregate to 1-minute resolution for storage. Compute SLI over the rolling window at query time. Raw storage stays manageable; query flexibility is preserved.

Multi-region services. Measure SLIs per region and aggregate. A regional outage should consume budget. If you only measure aggregate availability, a full regional failure looks like a small error rate and may never trigger alert thresholds.

SLA gap maintenance. As you tighten SLOs, re-check the gap to any external SLAs. An SLO violation should not immediately become an SLA breach. A reasonable minimum gap is 2x the worst incident duration from the last 12 months.


The goal is not to have impressive numbers. It is to have a shared definition of what “working” means, a measurement that tells you when you are failing that definition, and a policy that creates pressure to fix it before users notice. SLIs, SLOs, and SLAs are the operational vocabulary for that conversation. The vocabulary only matters if the team uses it consistently.

More in DevOps

How Terraform Works Internally: HCL Parsing, Dependency Graph Execution, and the Provider Protocol Behind Infrastructure as Code
DevOps ·

How Terraform Works Internally: HCL Parsing, Dependency Graph Execution, and the Provider Protocol Behind Infrastructure as Code

A deep dive into Terraform's internal architecture covering HCL parsing, the expression evaluation engine, DAG-based dependency resolution, the gRPC provider plugin protocol, state mechanics, and the plan/apply lifecycle.

How Docker Works Internally: Namespaces, Cgroups, Union Filesystems, and the OCI Runtime Behind Every Container
DevOps ·

How Docker Works Internally: Namespaces, Cgroups, Union Filesystems, and the OCI Runtime Behind Every Container

A deep dive into what happens when you run docker run: Linux namespaces for process isolation, cgroups v2 for resource enforcement, overlay2 union filesystem for layered images, the OCI runtime spec and how runc spawns containers, content-addressable storage, the containerd shim architecture, and networking with veth pairs.

How Kubernetes Works: Pods, Scheduling, and the Reconciliation Loop
DevOps ·

How Kubernetes Works: Pods, Scheduling, and the Reconciliation Loop

A deep dive into Kubernetes internals covering the control plane architecture, etcd watch mechanics, the scheduler's filter and score phases, controller manager reconciliation loops, kubelet pod assignment, the CRI and CNI plugin models, and production considerations for resource requests, pod disruption budgets, and cluster autoscaler behavior.

How Docker Works: Namespaces, Cgroups, Union Filesystems, and the Container Runtime from Build to Execution
DevOps ·

How Docker Works: Namespaces, Cgroups, Union Filesystems, and the Container Runtime from Build to Execution

A deep dive into Docker internals covering Linux namespace isolation, cgroup resource constraints, OverlayFS copy-on-write layer mechanics, Dockerfile build cache invalidation rules, the containerd and runc OCI runtime stack, and production considerations for image hardening and startup latency.