Skip to content

Safety Stock

Chapter summary

Safety stock (committed buffer in Mirabelle's terminology) is the inventory held above expected lead-time demand to absorb variability. This chapter derives the fundamental formulas, distinguishes cycle service level from fill rate, introduces lead-time variability, and explains how multi-echelon safety stock allocation differs from single-echelon approaches.


The fundamental problem

When a replenishment order is placed, it takes \(L\) time units (the lead time) before the inventory arrives. During those \(L\) periods, demand continues to arrive. If demand is exactly as forecast, the reorder point (ROP) equals the expected lead-time demand:

\[ ROP_{\text{no safety stock}} = \bar{d} \cdot L \]

where \(\bar{d}\) is the average demand per period.

But demand is stochastic. In the event that demand during lead time exceeds \(ROP\), a stockout occurs. Safety stock (SS) is the extra inventory above \(\bar{d}L\) held to limit the probability and impact of stockouts:

\[ ROP = \bar{d} \cdot L + SS \]

The sizing of \(SS\) is the central problem of inventory theory.


Basic safety stock — known, constant lead time

Figure: a replenishment cycle — on-hand decays from ROP through the lead-time window; safety stock SS is the cushion below expected lead-time demand that sets the protection level (CSL).

gantt
    title Replenishment cycle — on-hand vs lead-time demand
    dateFormat X
    axisFormat %s
    section Inventory
    On-hand at ROP            :done, inv1, 0, 1
    Demand consumes during L  :active, inv2, 1, 5
    Safety stock SS reached   :crit, inv3, 5, 6
    Replenishment arrives     :milestone, inv4, 6
    section Protection
    Expected LT demand d times L   :prot1, 1, 5
    Safety stock SS cushion        :crit, prot2, 5, 6

When lead time \(L\) is deterministic and demand per period is i.i.d. with mean \(\mu_d\) and standard deviation \(\sigma_d\), the demand over lead time \(D_{LT}\) has:

\[ E[D_{LT}] = L \cdot \mu_d, \quad \sigma_{LT} = \sigma_d \cdot \sqrt{L} \]

The \(\sqrt{L}\) scaling follows from the variance of a sum of \(L\) i.i.d. random variables: \(Var(D_{LT}) = L \cdot Var(D_1)\).

Cycle service level (CSL) approach

The cycle service level is the probability of no stockout during a replenishment cycle:

\[ CSL = P(D_{LT} \leq ROP) = P(D_{LT} \leq \mu_{LT} + SS) \]

For a Normal demand distribution:

\[ CSL = \Phi\!\left(\frac{SS}{\sigma_{LT}}\right) \implies SS = z_\alpha \cdot \sigma_{LT} \]

where \(z_\alpha = \Phi^{-1}(CSL)\) is the service-level factor. Common values:

CSL \(z_\alpha\)
90 % 1.282
95 % 1.645
97.5 % 1.960
99 % 2.326
99.9 % 3.090

This is the formula taught in textbooks and widely used in ERP systems. It has two important limitations:

  1. It assumes Normal demand over lead time.
  2. It targets per-cycle service, not the fraction of demand satisfied.

Fill rate vs. cycle service level

Definitions

Metric Definition
Cycle service level (CSL) \(P(\text{no stockout per replenishment cycle})\)
Fill rate (FR) \(\frac{\text{demand met from stock}}{\text{total demand}} = 1 - \frac{E[\text{backorders}]}{E[D_{LT}]}\)
Item fill rate As above, computed per order line (one unit = one line)
Unit fill rate As above, weighted by order quantity

CSL and FR are not equivalent. A CSL of 95 % does not mean 95 % of units are delivered on time — it means 95 % of cycles have no stockout. A single stockout in a cycle of 100 units gives CSL = 0 % but FR = 99 %.

FR is almost always the more meaningful metric for planners and is the default used by Mirabelle MEIO (line_fill_rate = true targets one unit = one order line).

Converting between CSL and FR (Normal demand)

The fill rate integral for Normal demand is:

\[ FR(SS) = 1 - \frac{\sigma_{LT}\cdot G\!\left(\tfrac{SS}{\sigma_{LT}}\right)}{\mu_{LT}} \]

where \(G(z)\) is the standard normal loss function:

\[ G(z) = \phi(z) - z\,[1 - \Phi(z)] \]

\(\phi\) is the standard normal PDF and \(\Phi\) is the standard normal CDF.

To achieve a target fill rate \(FR^*\), solve for \(SS\):

\[ G\!\left(\frac{SS}{\sigma_{LT}}\right) = \frac{(1 - FR^*)\,\mu_{LT}}{\sigma_{LT}} \]

This is solved numerically (the Rust optimizer uses a bisection loop or direct table lookup). For a given \(FR^*\), the implied \(z\) value is always lower than the \(z\) for the equivalent CSL, because FR requirements are less stringent than cycle service requirements.

Practical example

Suppose \(\mu_{LT} = 100\), \(\sigma_{LT} = 20\).

  • To achieve CSL = 95 %: \(SS = 1.645 \times 20 = 32.9\) units.
  • To achieve FR = 95 %: solve \(G(z) = (1-0.95)\times100/20 = 0.25\), giving \(z \approx 0.128\), whence \(SS = 0.128 \times 20 = 2.6\) units.

The difference is dramatic. FR = 95 % is easily achieved with a small buffer; CSL = 95 % requires much larger investment. This is why supply chain planners prefer fill-rate targets for most SKUs.


Lead-time variability

Figure: lead-time-demand variance decomposes into a demand-variability term (fixed L) and a lead-time-variability term; the combined σ_LT feeds the safety-stock formula.

flowchart TD
  DLTD["Demand over lead time<br/>D_LT"] --> SPLIT["Variance decomposition"]
  SPLIT --> DV["Demand variance term<br/>L̄ · σ_d²"]
  SPLIT --> LV["Lead-time variance term<br/>μ_d² · σ_L²"]
  DV --> SUM["σ_LT² = L̄·σ_d² + μ_d²·σ_L²"]
  LV --> SUM
  SUM --> SS["SS = z_α · σ_LT<br/>(Normal, CSL target)"]

In practice, lead time itself is stochastic. Suppliers deliver early or late; transit times vary. Ignoring lead-time variance understates the required safety stock, often significantly for MRO supply chains.

When both demand and lead time are random with: - Demand: mean \(\mu_d\), std \(\sigma_d\) per period - Lead time: mean \(\bar{L}\), std \(\sigma_L\) (in periods)

The variance of lead-time demand is:

\[ \sigma_{LT}^2 = \bar{L}\,\sigma_d^2 + \mu_d^2\,\sigma_L^2 \]

The first term is the demand variance component (variability of demand given a fixed lead time). The second term is the lead-time variance component (variability of total demand caused by the lead time being shorter or longer than average).

Lead-time variance dominates in MRO

For slow-moving spare parts with \(\mu_d\) large relative to \(\sigma_d\) (e.g., bulk consumables), the lead-time variance term \(\mu_d^2\sigma_L^2\) can be 5–10× larger than the demand variance term. Ignoring \(\sigma_L\) leads to chronic undersafety-stocking.

Figure: the two variability trees feeding σ_LT — demand variability (per-period σ_d scaled by √L̄) and lead-time variability (μ_d scaled by σ_L); in MRO the lead-time branch often dominates.

```mermaid mindmap root((σ_LT\nlead-time-demand std dev)) Demand variability per-period mean μ_d per-period std σ_d scales as σ_d · √L̄ dominates when demand is volatile Lead-time variability mean lead time L̄ lead-time std σ_L scales as μ_d · σ_L dominates in MRO spare parts Repair loop add-on repair TAT mean T_r repair TAT CV_r adds ργ · μ_d² · σ_Tr²

The combined standard deviation used in the safety-stock formula is therefore:

\[ \sigma_{LT} = \sqrt{\bar{L}\,\sigma_d^2 + \mu_d^2\,\sigma_L^2} \]

and the Normal-distribution safety stock becomes:

\[ SS = z_\alpha \cdot \sqrt{\bar{L}\,\sigma_d^2 + \mu_d^2\,\sigma_L^2} \]

Non-Normal distributions

For Gamma, Negative Binomial, Log-Normal, Weibull, and Poisson demand distributions, there is no closed-form expression for the fill-rate integral. Mirabelle evaluates:

\[ E[\max(D_{LT} - b,\, 0)] = \int_b^{\infty} [1 - F_{LT}(x)]\,dx \]

where \(F_{LT}\) is the lead-time CDF of the fitted distribution, scaled to the lead-time window (see the Distribution Fitting chapter for the scaling rules). This integral is computed numerically in the Rust optimizer's distributions.rs module for each candidate buffer level \(b\) during the greedy loop.


Multi-echelon safety stock

Why single-echelon is wrong

A single-echelon calculation treats each node in the supply network independently:

\[ SS_i = z_\alpha \cdot \sigma_{LT,i} \]

But in a multi-echelon network (central warehouse → regional → local), a stockout at the regional level means the local node must wait for the regional node to be replenished from central — the effective lead time at the local node is longer than its nominal replenishment lead time.

Conversely, if the regional warehouse holds safety stock and can immediately serve the local node on demand, the local node can operate with less safety stock (its effective lead time is shorter). The safety stocks are interdependent.

Echelon stock and the Clark–Scarf framework

The seminal insight (Clark and Scarf, 1960) is to replace on-hand stock with echelon stock — the total inventory at a node plus all downstream nodes:

\[ IS_i^{(e)} = IS_i + \sum_{j \in \text{downstream}(i)} IS_j \]

Clark and Scarf showed that a serial multi-echelon system can be decomposed into a set of independent single-echelon problems by sequential optimization from the most downstream node upward. Each node's effective lead time accounts for the upstream replenishment delay.

Mirabelle does not implement Clark–Scarf exactly (it is only tractable for serial systems). Instead, it uses a greedy marginal value allocation across all (item, site) pairs simultaneously, which is practical for general network topologies including kit dependencies and reverse logistics.

Committed buffer and wait time

In Mirabelle's MEIO, the safety stock at a node is called the committed buffer (\(b\)). The wait_time column in meio_results represents the expected delay (in weeks) a downstream node experiences when the upstream node is out of stock. It is computed recursively through the supply network:

\[ \text{wait\_time}_j = f\!\left(\text{committed\_buffer at upstream node},\; \text{leg\_lead\_time}_{ij},\; \text{demand distribution at node } i\right) \]

An upstream node with a higher committed buffer reduces the wait time seen by its downstream nodes, which in turn lowers the effective lead-time variance at those nodes and reduces their required safety stock.

Effective lead time at each echelon

For each (item, site) SKU in the network, the Rust optimizer computes an effective lead time:

\[ L_{\text{eff}} = L_{\text{leg}} + \text{wait\_time\_upstream} \]

where \(L_{\text{leg}}\) is the physical replenishment time for the route (transit + pick/pack, expressed in weeks). Changes in the upstream committed buffer propagate through repl_site_ids — the set of sites that supply the current node — causing the greedy loop to recompute marginal values for all affected downstream nodes after each allocation step.


Absolute stock lower bounds (ASL)

Before the greedy allocation begins, Mirabelle enforces hard minimum buffers for any SKU with:

  • sku_min_fill_rate > 0 — pre-compute the minimum buffer needed to achieve that fill rate and commit it.
  • sku_min_sl_qty > 0 — commit at least that many units unconditionally.

These minimums are set as SKU-level overrides in the MEIO scenario configuration. They ensure contractual service-level floors are met regardless of group budget constraints.


Asset mode (Erlang-B)

For high-value, low-volume repairable assets (MRO equipment), the fill-rate formulation is replaced by an availability model. The asset is always either in service or under repair; the "stockout" is a waiting asset.

The appropriate model is the Erlang-B loss formula (M/G/c/c queue):

\[ B(A,\,C) = \frac{A^C/C!}{\displaystyle\sum_{k=0}^{C}\frac{A^k}{k!}} \]

where: - \(A = \lambda\cdot\bar{R}\) is the offered load (demand rate \(\times\) mean repair turnaround time), - \(C\) is the stock level (number of spares in the pool).

Availability is:

\[ \text{Avail}(C) = 1 - B(A,\,C) \]

The optimizer increments \(C\) (integer stock level) from 0 upward and selects the smallest \(C\) that satisfies the availability target. This replaces the continuous SS = \(z \cdot \sigma_{LT}\) formula entirely.

Asset-mode scenarios are identified by service_level_type = 'availability' in the meio_scenarios table and routed through a separate Python path (_run_causal_asset_scenario) in meio_runner.py, which evaluates the Erlang-B formula directly without invoking the Rust optimizer.


Reverse logistics adjustment

When a RETURN/REPAIR route exists, the effective net demand is reduced:

\[ D_{\text{net}} = D_{\text{gross}} \cdot (1 - \rho \cdot \gamma) \]

where \(\rho\) is the return rate (fraction of demand that returns) and \(\gamma\) is the repair yield (fraction of returned units successfully repaired and returned to stock).

The repair turnaround time (mean \(\bar{T}_r\), coefficient of variation \(CV_r\)) adds a stochastic component to the effective lead time:

\[ \sigma^2_{LT,\,\text{with repair}} = \sigma^2_{LT} + \rho\gamma\,\mu_d^2\,\sigma^2_{T_r} \]

The net effect is that reverse logistics reduces both the mean demand (numerator of the fill-rate integral) and potentially reduces the required safety stock. The exact reduction depends on the return rate, yield, and repair turnaround variability.


Summary of formulas

Scenario Formula
Constant LT, Normal demand, CSL target \(SS = z_\alpha\,\sigma_d\sqrt{L}\)
Variable LT, Normal demand, CSL target \(SS = z_\alpha\sqrt{L\sigma_d^2 + \mu_d^2\sigma_L^2}\)
Variable LT, Normal demand, FR target Solve \(G(SS/\sigma_{LT}) = (1-FR^*)\mu_{LT}/\sigma_{LT}\)
Non-Normal demand, FR target Numeric integration of \(E[\max(D_{LT}-b,0)]\)
Asset mode \(C = \min\{C : 1 - B(\lambda\bar{R}, C) \geq \text{Avail}^*\}\)

Cross-references: Distribution Fitting · MEIO Theory · K-Curve