- FFT detects bearing faults by identifying peaks at calculable fault frequencies (BPFO, BPFI) derived from bearing geometry and shaft speed.
- A 2048-point window at 12.8 kHz sampling gives 6.25 Hz frequency resolution, sufficient to separate fault peaks from harmonics and electrical noise.
- Hann windowing reduces spectral leakage but cuts amplitude by 50% — apply the same window consistently to track fault progression over time.
- FFT works for developed faults with periodic impacts; early-stage cracks (<1 mm) with low SNR require envelope analysis or spectral kurtosis.
A Low-Frequency Spike That Shouldn’t Exist
You’ve mounted accelerometers on a motor bearing, set your sampling rate to 12.8 kHz, and started collecting vibration data. The motor’s nameplate says 1800 RPM (30 Hz), so you’re expecting peaks at the rotational frequency and maybe some harmonics. You run an FFT and see a clean peak at 30 Hz. Good.
Then you notice a second peak at 156 Hz that’s 40% the amplitude of the main frequency. That’s not a harmonic. It’s not matching any gear mesh frequency either.
That’s your bearing’s outer race fault frequency.
This is what FFT analysis does for bearing diagnostics — it turns raw vibration time-series into a frequency spectrum where specific fault patterns show up as distinct peaks. But getting from “I see a spike” to “this is an outer race defect” requires understanding what you’re looking for and why half the tutorials get the window function wrong.

Why Bearing Faults Have Predictable Frequencies
Bearing defects generate periodic impacts. A crack on the outer race gets hit every time a rolling element passes over it. The impact creates a transient vibration pulse that repeats at a calculable rate.
The outer race fault frequency is:
where is the number of rolling elements, is the shaft rotation frequency, is the ball diameter, is the pitch diameter, and is the contact angle.
For a typical deep-groove ball bearing with , , and , this simplifies to roughly $3.6 \times f_r because the inner race is moving.
The CWRU bearing dataset uses 6205-2RS bearings with published geometry. The outer race fault frequency is $3.5848 \times f_r$. At 1797 RPM (29.95 Hz), that’s 107.36 Hz. If your FFT shows a peak there with sidebands spaced at the rotation frequency, you’ve found your fault.
The 2048-Point Window Nobody Explains
Most FFT tutorials default to a window size of 1024 or 2048 samples without explaining why. Here’s what actually matters.
Your frequency resolution is:
where is your sampling rate and is the FFT size (window length). At 12.8 kHz with , you get Hz. That means every bin in your spectrum is 6.25 Hz wide.
If your bearing fault frequency is 107 Hz and your bin width is 6.25 Hz, the fault peak will land in bin 17 (106.25 Hz) or bin 18 (112.5 Hz). You’ll see it. But if you used , your resolution would be 25 Hz — the fault peak and the 120 Hz electrical noise from the motor controller would smear together.
The tradeoff: longer windows give better frequency resolution but worse time resolution. With at 12.8 kHz, each FFT covers 160 ms of data. If your fault signature only lasts 50 ms (early-stage crack), you’re averaging it with 110 ms of normal vibration. The peak gets weaker.
For bearing diagnostics, I’d start with for exploratory analysis. If you’re tracking fault progression over weeks, you want consistent bin alignment — don’t change mid-campaign.
Hann Window vs Rectangular: The Spectral Leakage Nobody Sees
Here’s a mistake I see in production systems: applying FFT without windowing.
import numpy as np
from scipy.fft import fft, fftfreq
import matplotlib.pyplot as plt
# Generate 30 Hz sine wave (rotation frequency)
fs = 12800 # 12.8 kHz sampling
t = np.arange(0, 0.16, 1/fs) # 160 ms window
rotation = np.sin(2 * np.pi * 30 * t)
# Add bearing fault: 107 Hz (outer race), 20% amplitude
fault = 0.2 * np.sin(2 * np.pi * 107 * t)
signal = rotation + fault + 0.1 * np.random.randn(len(t)) # Add noise
# FFT without windowing (rectangular)
spectrum_rect = np.abs(fft(signal))[:len(signal)//2]
freqs = fftfreq(len(signal), 1/fs)[:len(signal)//2]
# FFT with Hann window
window = np.hanning(len(signal))
spectrum_hann = np.abs(fft(signal * window))[:len(signal)//2]
If you plot both spectra, the rectangular window shows frequency leakage — the 30 Hz peak bleeds into adjacent bins because your time window cuts the sine wave at arbitrary points. The Hann window tapers the signal to zero at the edges, reducing this artifact.
But here’s the catch: the Hann window reduces amplitude by roughly 50%. If you’re comparing fault peak heights week-over-week, you need to apply the same window every time. Otherwise your “fault is growing” might just be “I forgot to apply Hann this time.”
The amplitude correction factor for Hann is:
That 0.5 is the coherent gain of the Hann window. The Hamming window uses 0.54. Don’t mix them.

Real Bearing Signals Are Nonstationary (And FFT Doesn’t Care)
FFT assumes your signal is stationary — same frequency content throughout the window. Bearing faults violate this. The impact from a crack is a transient burst lasting maybe 5-10 ms, then the signal returns to baseline until the next rolling element hits the defect.
If your 160 ms window captures one impact at ms and another at ms, the FFT averages them. You’ll see the fault frequency, but you lose information about impact sharpness (kurtosis). That’s why envelope analysis beats FFT for early-stage faults — it demodulates the high-frequency resonance excited by impacts before doing the FFT.
But envelope analysis requires bandpass filtering around the bearing’s resonance frequency, which depends on sensor mounting, bearing type, and load. For an ADXL335 MEMS accelerometer (commonly used in DIY setups), the resonance is around 5-7 kHz. For an industrial ICP accelerometer like the PCB 353B15, it’s 40+ kHz.
If you don’t know your sensor’s resonance, start with FFT. It’s robust.
Setting Up the Full Pipeline
Here’s a realistic implementation for a batch of vibration files.
import numpy as np
from scipy.fft import fft, fftfreq
from scipy.signal import butter, filtfilt
import pandas as pd
def load_vibration_data(filepath):
"""Load CSV with 'timestamp' and 'accel_g' columns."""
df = pd.read_csv(filepath)
return df['accel_g'].values
def compute_bearing_fft(signal, fs=12800, nperseg=2048,
window='hann', detrend=True):
"""Compute FFT with preprocessing.
Args:
signal: 1D array, acceleration in g
fs: sampling frequency in Hz
nperseg: FFT window length (power of 2 recommended)
window: 'hann', 'hamming', 'blackman', or 'rect'
detrend: remove DC offset before FFT
Returns:
freqs: frequency bins (Hz)
magnitude: amplitude spectrum (g)
"""
if detrend:
signal = signal - np.mean(signal) # Remove DC offset
# Apply window
if window == 'hann':
w = np.hanning(nperseg)
coherent_gain = 0.5
elif window == 'hamming':
w = np.hamming(nperseg)
coherent_gain = 0.54
elif window == 'rect':
w = np.ones(nperseg)
coherent_gain = 1.0
else:
raise ValueError(f"Unknown window: {window}")
# Zero-pad if signal is shorter than nperseg
if len(signal) < nperseg:
signal = np.pad(signal, (0, nperseg - len(signal)), mode='constant')
# Take first nperseg samples (or average multiple windows in production)
segment = signal[:nperseg] * w
spectrum = fft(segment)
magnitude = np.abs(spectrum[:nperseg//2]) * (2.0 / (nperseg * coherent_gain))
freqs = fftfreq(nperseg, 1/fs)[:nperseg//2]
return freqs, magnitude
def detect_fault_peak(freqs, magnitude, fault_freq, tolerance=10):
"""Check if a peak exists near the expected fault frequency.
Args:
fault_freq: expected fault frequency in Hz (e.g., 3.58 * shaft_speed)
tolerance: search window in Hz (accounts for speed variation)
Returns:
detected: bool
peak_freq: actual peak frequency if detected
peak_amp: peak amplitude in g
"""
# Find indices within tolerance
mask = (freqs >= fault_freq - tolerance) & (freqs <= fault_freq + tolerance)
if not np.any(mask):
return False, None, None
local_spectrum = magnitude[mask]
local_freqs = freqs[mask]
peak_idx = np.argmax(local_spectrum)
peak_amp = local_spectrum[peak_idx]
peak_freq = local_freqs[peak_idx]
# Require peak to be 3x the median of surrounding bins (simple threshold)
median_noise = np.median(magnitude)
if peak_amp > 3 * median_noise:
return True, peak_freq, peak_amp
return False, peak_freq, peak_amp
# Example usage
data = load_vibration_data('bearing_run_001.csv') # Should have 20k+ samples
freqs, magnitude = compute_bearing_fft(data, fs=12800, nperseg=2048)
# For 1800 RPM motor with standard 6205 bearing
shaft_freq = 1800 / 60 # 30 Hz
BPFO = 3.5848 * shaft_freq # Outer race fault frequency
detected, peak_f, peak_a = detect_fault_peak(freqs, magnitude, BPFO, tolerance=10)
if detected:
print(f"Outer race fault detected: {peak_f:.2f} Hz, {peak_a:.4f} g")
else:
print("No fault detected")
This handles DC offset removal (critical for MEMS accelerometers with 1.65V bias), window correction, and zero-padding for short signals. The detect_fault_peak function is deliberately simple — in production you’d use peak prominence from scipy.signal.find_peaks and compare to a baseline spectrum from healthy bearings.
Edge Cases That Break FFT Diagnostics
Variable speed. If your motor ramps from 1200 to 1800 RPM during the measurement window, the fault frequency smears across 20+ bins. You won’t see a sharp peak. Solution: segment the signal into constant-speed chunks or use order tracking (resample to angular domain).
Multiple faults. A bearing with both inner and outer race defects produces overlapping peaks. The inner race fault at $5.4 \times f_r = 162N$ or envelope analysis to separate them.
Low SNR in early faults. A crack 0.5 mm wide might generate impacts only 0.01 g above the 0.05 g noise floor. The FFT peak will be buried. Dark Chocolate Espresso Beans won’t fix this — you need envelope analysis or spectral kurtosis (which I covered in a previous post).
Electrical noise. 60 Hz (or 50 Hz in Europe) power line harmonics show up as strong peaks at 60, 120, 180 Hz. If your bearing fault frequency is 118 Hz, you’ll see a peak — but it might just be the second harmonic of electrical noise. Always measure a reference signal with the motor off to characterize your baseline noise.
Computational Constraints: Embedded vs Cloud
FFT is computationally cheap. A 2048-point FFT on a Raspberry Pi 4 (ARM Cortex-A72) takes ~0.3 ms using NumPy’s fft (which calls FFTPACK under the hood). On an ESP32 (240 MHz dual-core), the same operation takes ~15 ms if you use a fixed-point FFT library like kiss_fft.
For real-time monitoring at 1 Hz (one FFT per second), even an ESP32 can handle it. But if you’re streaming 12.8 kHz data continuously, you need to batch — collect 2048 samples (160 ms), compute FFT, then repeat. That’s 6.25 FFT/sec, well within budget.
The bandwidth bottleneck is data transmission. At 12.8 kHz with 16-bit samples, you’re generating 25.6 kB/s. Over a 4G LTE connection (10 Mbps uplink), that’s negligible. But if you’re on LoRaWAN (50 kbps max), you can’t stream raw data — you need to compute FFT on-device and transmit only the spectrum (2048 floats = 8 kB per FFT, or 50 kB/s at 6.25 FFT/sec). Still too much. You’d have to downsample the spectrum or transmit only peaks above a threshold.
For edge deployment, I’d compute FFT locally and send summary statistics: peak frequency, peak amplitude, RMS of the spectrum from 50-500 Hz. That’s 12 bytes per measurement. Totally feasible.
When FFT Isn’t Enough
FFT works when faults are developed enough to produce periodic impacts with consistent amplitude. It fails for:
- Early-stage faults where impacts are sporadic or low-amplitude (SNR < 3 dB)
- Non-stationary loads where the shaft speed varies more than 5% during the window
- High-frequency resonances that FFT can’t resolve without 50+ kHz sampling (industrial piezoelectric sensors only)
In those cases, you’d move to envelope analysis (bandpass filter around bearing resonance, demodulate, then FFT) or wavelet transforms (which adapt window size to frequency). But those require more domain knowledge — you need to know your sensor’s resonance band, and envelope analysis can produce false positives if you pick the wrong filter.
FFT is the starting point. If you see a clear peak at the calculated fault frequency, you don’t need anything fancier.
FAQ
Q: What sampling rate should I use for bearing diagnostics?
ISO 10816 recommends at least 2.5x the maximum frequency of interest. For bearing fault detection, that’s typically the ball pass frequency outer race (BPFO), which ranges from 100-500 Hz depending on shaft speed. A 12.8 kHz sampling rate gives you useful data up to 6.4 kHz (Nyquist), which covers all standard bearing fault frequencies plus several harmonics. If you’re using envelope analysis, you need to capture the bearing resonance (5-40 kHz depending on sensor), so 50+ kHz is better.
Q: How do I know if a peak is a real fault or just noise?
Compare to a baseline spectrum from a healthy bearing under the same load and speed. Real faults show peaks at predictable frequencies (BPFO, BPFI, BSF) with sidebands spaced at the shaft rotation frequency. Random noise doesn’t have sidebands. Also check peak prominence — fault peaks should be at least 3x the median noise floor. If you’re seeing peaks everywhere, your SNR is too low.
Q: Can I use FFT on variable-speed motors?
Not directly. Variable speed smears fault frequencies across multiple bins. You need order tracking, which resamples the time-domain signal into the angular domain (samples per revolution instead of samples per second). This requires an encoder or tachometer signal. If you don’t have that, segment your data into short windows where speed is approximately constant (within 1-2%) and compute FFT on each segment.
Pick Your Window Size and Stick With It
If you’re setting up a bearing monitoring system, start with 12.8 kHz sampling and a 2048-point Hann window. That gives you 6.25 Hz resolution, which is tight enough to separate bearing fault frequencies from harmonics but coarse enough to tolerate ±5% speed variation. Compute one FFT per second, log the peak amplitude at your calculated BPFO, and track it over weeks.
When the peak amplitude doubles from baseline, schedule an inspection. When it hits 4x, plan a replacement.
The thing I still haven’t solved: how to reliably detect early faults (crack <1 mm) in noisy industrial environments without envelope analysis. FFT sees them once they’ve grown to 2-3 mm, but by then you’ve lost a month of warning time. Spectral kurtosis helps, but it’s sensitive to transient electrical noise. If you’ve figured this out, I’d like to hear about it.
Did you find this helpful?
Your support keeps this blog running and ad-free content coming.
☕ Buy me a coffeeMost Popular Posts
- Custom Metaclass in Python: 43% Faster Validation (12,796 views)
- Python match-case: 7 Patterns That Beat if-elif Chains (947 views)
- YOLOv8 INT8 Quantization: 4x Faster on Jetson Orin (763 views)
- yfinance Alternatives 2026: 7 Free APIs Compared (657 views)
- PaddleOCR vs EasyOCR vs Tesseract: Why PaddleOCR Is Slower (552 views)