FDA 21 CFR Part 820 requires medical device manufacturers to maintain a Device History Record (DHR) for every unit produced. Capturing test results, serial numbers, measurements, and pass/fail determinations in an immutable record maps directly to FDA and ISO 13485 requirements.
Regulatory Requirements Overview
Medical device test traceability sits at the intersection of two frameworks. Both require documented evidence that each device was manufactured and tested according to approved procedures.
| Regulation | Clause | Requirement | Where it maps |
|---|---|---|---|
| 21 CFR 820.184 | DHR | Records demonstrating device manufactured per DMR | Test records per serial number with procedure name |
| 21 CFR 820.80 | Receiving, in-process, finished device acceptance | Acceptance activities with acceptance/rejection documented | Pass/fail results with measurement limits |
| 21 CFR 820.90 | Nonconforming product | Documented investigation of nonconformances | Failed runs with measurement-level detail |
| 21 CFR 820.180 | General requirements (records) | Records maintained for device lifetime or 2 years | Immutable cloud storage with retention |
| 21 CFR 820.250 | Statistical techniques | Valid statistical techniques for process capability | Cpk, control charts, first pass yield |
| ISO 13485 7.5.3 | Traceability | Records per unit or batch through production | Serial tracking, unit history, sub-assembly links |
| ISO 13485 8.2.4 | Monitoring and measurement | Evidence of conformity to acceptance criteria | Measurements with units, limits, and pass/fail |
This table maps requirements to where the evidence lives. It is not a compliance claim: your quality system, validation, and procedures are what satisfy the regulation, and the test records are one input to that. Software used in production also falls under 21 CFR 820.70(i), so the tooling itself needs validation for its intended use.
Structuring Tests for DHR Compliance
The DHR must include test results that prove each device was manufactured according to its Device Master Record (DMR). Structure your OpenHTF tests so each procedure maps to a documented test protocol.
test_medical_device.py74 lines
# Medical device production test aligned with DHR requirementsimport openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures( htf.Measurement("impedance_channel_1") .in_range(minimum=980, maximum=1020) .with_units(units.OHM) .doc("Electrode impedance per IEC 60601-1, channel 1"), htf.Measurement("impedance_channel_2") .in_range(minimum=980, maximum=1020) .with_units(units.OHM) .doc("Electrode impedance per IEC 60601-1, channel 2"), htf.Measurement("leakage_current") .in_range(maximum=0.00001) .with_units(units.AMPERE) .doc("Patient leakage current, Type BF applied part per IEC 60601-1"),)def electrical_safety(test): test.measurements.impedance_channel_1 = 1002.3 test.measurements.impedance_channel_2 = 997.1 test.measurements.leakage_current = 0.000003@htf.measures( htf.Measurement("signal_accuracy_pct") .in_range(minimum=99.0, maximum=101.0) .doc("Measurement accuracy against NIST-traceable reference"), htf.Measurement("noise_floor") .in_range(maximum=0.000005) .with_units(units.VOLT) .doc("Input-referred noise, 0.05-150 Hz bandwidth"), htf.Measurement("common_mode_rejection") .in_range(minimum=100) .doc("CMRR at 50/60 Hz, in dB"),)def signal_performance(test): test.measurements.signal_accuracy_pct = 99.8 test.measurements.noise_floor = 0.0000021 test.measurements.common_mode_rejection = 112.4@htf.measures( htf.Measurement("firmware_version") .doc("Installed firmware version, must match DMR revision"), htf.Measurement("self_test_result") .in_range(minimum=1, maximum=1) .doc("Device self-test pass (1) or fail (0)"), htf.Measurement("battery_capacity_pct") .in_range(minimum=95) .doc("Battery capacity relative to rated"),)def system_validation(test): test.measurements.firmware_version = "3.2.1-release" test.measurements.self_test_result = 1 test.measurements.battery_capacity_pct = 98.7def main(): test = htf.Test( electrical_safety, signal_performance, system_validation, procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", # procedure UUID from the dashboard part_number="MD-100", ) test.add_output_callbacks(upload()) test.execute(lambda: "MD-2026-04-00192")if __name__ == "__main__": main()Install it with pip install "tofupilot[openhtf]". Each measurement references the applicable standard through .doc() and carries its acceptance limits, so the stored record shows what was required as well as what was measured.
Version the procedure whenever limits change. A DHR is only defensible if you can say which acceptance criteria applied to a given unit on the day it was tested, and that becomes impossible once limits have moved without a version marker.
Acceptance Activities (21 CFR 820.80)
The FDA requires documented acceptance activities at receiving, in-process, and finished device stages. Run separate procedures for each stage, all using the same serial number.
test_acceptance_stages.py64 lines
# Three-stage acceptance testing for FDA 820.80 complianceimport openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import uploadRECEIVING_PROCEDURE_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"IN_PROCESS_PROCEDURE_ID = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"FINISHED_PROCEDURE_ID = "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz"@htf.measures( htf.Measurement("incoming_visual_pass") .in_range(minimum=1, maximum=1), htf.Measurement("incoming_dimension_check") .in_range(minimum=24.9, maximum=25.1) .with_units(units.MILLIMETRE),)def receiving_inspection(test): test.measurements.incoming_visual_pass = 1 test.measurements.incoming_dimension_check = 25.02@htf.measures( htf.Measurement("solder_paste_height") .in_range(minimum=0.10, maximum=0.15) .with_units(units.MILLIMETRE), htf.Measurement("component_presence") .in_range(minimum=1, maximum=1),)def in_process_inspection(test): test.measurements.solder_paste_height = 0.12 test.measurements.component_presence = 1@htf.measures( htf.Measurement("final_functional_test") .in_range(minimum=1, maximum=1), htf.Measurement("label_verification") .in_range(minimum=1, maximum=1),)def finished_device_acceptance(test): test.measurements.final_functional_test = 1 test.measurements.label_verification = 1def run_stage(phase, procedure_id, serial): test = htf.Test( phase, procedure_id=procedure_id, part_number="MD-100", ) test.add_output_callbacks(upload()) test.execute(lambda: serial)def main(): serial = "MD-2026-04-00192" run_stage(receiving_inspection, RECEIVING_PROCEDURE_ID, serial) run_stage(in_process_inspection, IN_PROCESS_PROCEDURE_ID, serial) run_stage(finished_device_acceptance, FINISHED_PROCEDURE_ID, serial)if __name__ == "__main__": main()The three stages run on separate procedures so each can be reported independently, while the shared serial number keeps them on one unit history. In production these are three scripts on three stations, days apart; they are shown together here only to make the pattern visible.
The unit page for this serial shows all three acceptance stages in order, which is what an inspector asks to see.
Nonconforming Product (21 CFR 820.90)
When a device fails testing, the FDA requires documented investigation. The record holds the exact measurement that triggered the failure, its value, and the limit it exceeded.
Failed runs stay visible in the unit's history alongside every retest, so the investigation, rework, and retest cycle is traceable. That covers the record-keeping side of 820.90. The investigation and disposition decisions themselves live in your CAPA system; the test data is evidence feeding it, not a substitute for it.
Component Traceability with Sub-Units
ISO 13485 clause 7.5.3 requires traceability of components used in each device. sub_units is a field on htf.Test(...) that links component serial numbers to the finished device.
test_component_traceability.py30 lines
# Record component serials for ISO 13485 traceabilityimport openhtf as htffrom tofupilot.openhtf import upload@htf.measures( htf.Measurement("assembly_complete") .in_range(minimum=1, maximum=1),)def final_assembly(test): test.measurements.assembly_complete = 1def main(): test = htf.Test( final_assembly, procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", part_number="MD-100", sub_units=[ {"serial_number": "SENSOR-2026-08812"}, {"serial_number": "PCBA-2026-04410"}, {"serial_number": "BATT-2026-11023"}, ], ) test.add_output_callbacks(upload()) test.execute(lambda: "MD-2026-04-00192")if __name__ == "__main__": main()The serials are hardcoded here for brevity. On a real line they come from barcode scans collected before the test is constructed, since sub_units is a constructor argument.
If a component supplier issues a recall, the unit hierarchy is what tells you which finished devices contain the affected parts, rather than quarantining everything built in a date range.
Audit Trail and Exports
The records support what FDA and ISO 13485 auditors ask for:
- Device History Record. Every test run for a serial number, with procedure, station, operator, measurements, and result. This forms the test portion of the DHR.
- Process capability. Cpk values and control charts for any measurement, supporting statistical process control per 21 CFR 820.250.
- Failure analysis. Failure Pareto and measurement histograms to support CAPA investigations.
- Record retention. Immutable storage, with historical records not alterable through the standard interface.
Export test data from the dashboard for inclusion in your formal DHR documentation package. Records include timestamps, station identifiers, and all measurement values with their acceptance criteria.
Confirm retention periods against your own device lifetime requirement, and keep an export path that does not depend on a single vendor. 820.180 obligations outlive any particular tool.
