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:
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:
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:
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:
For a Normal demand distribution:
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:
- It assumes Normal demand over lead time.
- 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:
where \(G(z)\) is the standard normal loss function:
\(\phi\) is the standard normal PDF and \(\Phi\) is the standard normal CDF.
To achieve a target fill rate \(FR^*\), solve for \(SS\):
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:
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:
and the Normal-distribution safety stock becomes:
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:
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:
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:
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:
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:
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):
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:
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:
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:
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