First pass yield (FPY) is the proportion of units that pass a test stage on their first attempt, with no rework and no retest. It is the most quoted number in manufacturing test and the most commonly miscounted.
The Definition
FPY = units passing on the first attempt / units entering the stage
The denominator is units that entered, not units that eventually passed. The numerator counts first attempts only. A board that fails, gets reworked, and passes on the second run is a failure for FPY purposes, even though it ships.
That last sentence is where most disagreements start, and it is the whole point of the metric. FPY measures whether the process produces good boards. Final yield measures whether you managed to ship them. Both are useful; conflating them hides the cost of rework.
FPY vs the Other Yields
| Metric | Counts | Answers |
|---|---|---|
| First pass yield | Passed first time / entered | Is the process right the first time? |
| Final yield | Passed eventually / entered | What fraction shipped? |
| Rolled throughput yield (RTY) | Product of FPY across all stages | What fraction passes every stage untouched? |
| Throughput | Units per hour | Is the line fast enough? |
A line with 92 % FPY and 99 % final yield is spending real money on rework that the final number conceals. See rolled throughput yield for the multi-stage view.
What Corrupts the Number
Retests overwriting the original. If a retest replaces the failing record, FPY silently becomes final yield. The failing run has to survive.
Operators re-running until it passes. Three attempts and a pass is not a pass; without the first two recorded, it looks like one.
Blending stages. ICT, FCT and final inspection have different failure modes. One combined number tells you the line is at 94 % and nothing about where to look.
Excluding "not real" failures. Fixture contact failures and operator errors are real production events that cost time. Excluding them from FPY produces a number that flatters the line and hides a fixture problem.
Counting reworked units as new units. A reworked board re-entering the stage inflates the denominator and makes yield look better with every rework cycle.
Getting It Right in the Data
FPY is a property of your records, not a calculation you apply afterwards. Three things have to be true.
One run per attempt. Every execution uploads, whether it passed or failed. Failures are data.
A stable unit identity. Every run carries the serial number, so attempt order per unit is reconstructable.
One procedure per stage. Separate procedures for ICT, FCT and final gives you three FPY numbers that mean something.
With those, FPY is a query rather than a spreadsheet exercise.
procedure.yaml29 lines
name: PCBA Functional Testversion: 1.0.0unit: serial_number: default_value: "PCBA000001" part_number: default_value: "PCBA-100"plugs: - name: dmm python: plugs.dmm:Multimetermain: - name: Power Rails python: phases.power_rails measurements: - name: Rail 3V3 unit: V validators: - operator: ">=" expected_value: 3.2 - operator: "<=" expected_value: 3.4 - name: Quiescent Current unit: mA validators: - operator: "<=" expected_value: 45.0def power_rails(measurements, dmm): measurements.rail_3v3 = dmm.read_voltage() measurements.quiescent_current = dmm.read_current_ma()class Multimeter: def read_voltage(self) -> float: return 3.31 # Replace with a real instrument read def read_current_ma(self) -> float: return 41.3Run it with tofupilot run ./procedure.yaml --upload. Each attempt on a serial becomes its own run, in order, pass or fail.
Marking Retests Honestly
When a board is reworked and re-tested, say so on the run. That makes the distinction between first attempts and retests explicit rather than inferred:
def record_attempt(run, attempt_type): """attempt_type comes from an operator prompt: 'first' or 'retest'.""" run.metadata["attempt_type"] = attempt_type run.metadata["rework_ref"] = ""If the rework had a work order, record its reference too. A month later, "which rework action actually fixed anything" becomes answerable.
Reading FPY
| Question | What to look at |
|---|---|
| What is FPY this shift? | First pass yield on the procedure, filtered by date |
| Which measurement costs the most units? | Pareto of failing measurements |
| Is one station worse? | FPY filtered by station or slot |
| Is a measurement drifting toward its limit? | Control chart on that measurement |
| Are limits too tight for the process? | Cpk per measurement |
| How much rework are we doing? | FPY compared against final yield |
The gap between FPY and final yield is the rework bill. If FPY is 92 % and final is 99.5 %, 7.5 % of production is being touched twice, and that cost is invisible in the final number.
A Falling FPY Is a Diagnosis Problem, Not a Number Problem
When FPY drops, the number itself tells you nothing about why. The useful sequence is:
- Which measurement started failing? The Pareto usually names one.
- Did the values shift, or did the spread widen? A shifted mean is usually a process or supplier change; a widened spread is usually a fixture or measurement problem.
- Is it one station or all of them? Filter by station. One station means fixture, cabling, or instrument calibration.
- When did it start? Line up the change against build records, paste lots, stencil changes, firmware releases.
- Were the limits changed? A tightened limit produces a yield drop with no process change at all, which is why procedure versioning matters.
If You Already Run OpenHTF
Existing OpenHTF suites produce the same per-attempt records on the TofuPilot CLI:
import openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures( htf.Measurement("rail_3v3") .in_range(3.2, 3.4) .with_units(units.VOLT),)def phase_power(test): test.measurements.rail_3v3 = 3.31test = htf.Test(phase_power, part_number="PCBA-100")test.add_output_callbacks(upload())test.execute(test_start=lambda: input("Scan serial: "))Install with pip install "tofupilot[openhtf]", or skip the callback and run it with tofupilot run.
Common Mistakes
| Mistake | Consequence | Fix |
|---|---|---|
| Overwriting a failed run with the retest | FPY silently becomes final yield | Keep every attempt as its own run |
| One blended yield across stages | No idea which stage is failing | One procedure per stage |
| Excluding fixture failures | A fixture problem stays invisible | Count them; they cost real time |
| Counting reworked units in the denominator again | Yield improves with every rework cycle | Denominator is units entering, once |
| No serial number on the run | Attempt order cannot be reconstructed | Always identify the unit |
| Chasing the number instead of the measurement | Yield meetings with no root cause | Start from the failing measurement's distribution |
