How to Connect Bluetooth Headphones and Earbuds on Fedora

Connect Bluetooth headphones on Fedora by starting the bluetooth service and using bluetoothctl to pair and connect to your device.

The scenario

You just unboxed a new pair of wireless earbuds. You press the pairing button, open Fedora, and the device either vanishes from the list or connects but refuses to play sound. You have tried restarting the laptop. You have tried turning Bluetooth off and on. The desktop sound settings show the headphones as available but nothing routes to them. This is a common friction point on Fedora. The desktop environment handles the visual pairing, but the actual audio path relies on a separate daemon and a media session manager. When they fall out of sync, you get silence.

What is actually happening under the hood

Fedora splits Bluetooth responsibilities between two distinct subsystems. BlueZ handles the radio, device discovery, and cryptographic pairing. PipeWire handles the audio stream, codec negotiation, and device routing. Think of BlueZ as the handshake that establishes trust between two radios. Think of PipeWire as the sound engineer that decides which microphone or speaker gets the audio feed. When you pair a device, BlueZ exchanges keys and stores them in /var/lib/bluetooth/. When you connect, BlueZ opens the radio channel. PipeWire then asks BlueZ for the available profiles, negotiates a codec, and routes the application streams to the correct sink.

The desktop settings panel talks to both daemons through D-Bus. It works well for simple cases. It also hides the actual state transitions. When the GUI shows connected but audio plays through the laptop speakers, PipeWire has not switched the default sink. When the GUI shows failed to connect, BlueZ is usually rejecting the profile or the radio is stuck in a low-power state. Understanding the separation lets you fix the problem without guessing.

Bluetooth audio uses two primary profiles. A2DP handles high-quality stereo playback. HFP or HSP handles voice calls and microphone input. PipeWire automatically switches between them based on application requests. A music player requests A2DP. A video conferencing app requests HFP. The switch happens in milliseconds, but some cheap earbuds struggle with the transition and drop the connection. Knowing which profile is active explains why your headphones sound great for music but cut out during calls.

Convention aside: journalctl -xe reads better than journalctl alone. The x flag adds explanatory text and the e flag jumps to the end. Most sysadmins type journalctl -xeu <unit> muscle-memory style. Use it to watch BlueZ and PipeWire negotiate in real time.

Check the radio state before you start pairing. Half the time the symptom is gone after a clean service restart.

The standard pairing procedure

Start by confirming the Bluetooth daemon is active and listening. The desktop environment usually starts it automatically, but a clean install or a recent kernel update can leave it disabled.

systemctl status bluetooth
# Check the active state and recent log lines before assuming it is broken

If the service is inactive, start it and enable it for future boots.

sudo systemctl start bluetooth
# Launch the daemon immediately for the current session
sudo systemctl enable bluetooth
# Register the service in the default target so it survives reboots

Put your headphones into pairing mode. This usually means holding the power button until an LED flashes blue or white. The exact sequence varies by manufacturer. Open a terminal and launch the interactive controller.

bluetoothctl
# Drop into the BlueZ interactive shell with root privileges implied for pairing

Inside the controller, run the following sequence. Replace the MAC address with the one shown during scanning.

scan on
# Instruct the radio to broadcast inquiry packets and listen for responses
pair 00:1A:7D:DA:71:13
# Exchange encryption keys and verify the device accepts the connection
trust 00:1A:7D:DA:71:13
# Mark the device as trusted so BlueZ auto-connects when in range
connect 00:1A:7D:DA:71:13
# Open the active radio channel and request the default profile
exit
# Leave the interactive shell and return to your normal terminal

The trust command is the step most users skip. Without it, BlueZ treats the device as temporary. It will disconnect after a few minutes of inactivity and refuse to reconnect automatically. Trusting the device writes a persistent flag to the BlueZ configuration database.

Convention aside: systemctl status <unit> shows recent log lines AND state in one view. Always check status before restart. Restarting a healthy daemon clears its cache and can drop active connections.

Run the trust command before you walk away from the desk. Future-you will not have to pair the same earbuds twice.

Verify the connection and audio routing

Pairing and connecting are not the same as routing audio. PipeWire manages the actual sound path. Open a terminal and check the current default sink.

pactl info | grep "Default Sink"
# Query PulseAudio compatibility layer for the active audio output device

If the output shows your laptop speakers or an HDMI port, switch the default sink to the Bluetooth device.

pactl set-default-sink bluez_sink.00_1A_7D_DA_71_13
# Tell PipeWire to route all new application streams to the paired headset

The sink name follows a predictable pattern. The MAC address gets converted to underscores. You can list all available sinks to find the exact identifier.

pactl list short sinks
# Print a compact table of all audio outputs recognized by PipeWire

Play a test sound to confirm routing.

speaker-test -t wav -c 2
# Generate a stereo WAV tone to verify the audio path is active

If you hear the tone through the headphones, the stack is working. If you hear it through the laptop speakers, PipeWire has not applied the sink switch. Run the set-default-sink command again and check for typos in the MAC address.

Check the default sink before you blame the headphones. The radio is usually fine. The routing table is rarely.

Common pitfalls and error patterns

The most frequent failure is the device appearing in the scan but refusing to pair. BlueZ will print a rejection message in the interactive shell.

[CHG] Device 00:1A:7D:DA:71:13 Connected: yes
Failed to pair: org.bluez.Error.Rejected

This error means the headphones are already paired to another device or are not in true pairing mode. Many earbuds enter a quick connect mode that only works with the last paired phone. Hold the button longer until the LED pattern changes to the factory pairing sequence.

Another common issue is the connection dropping immediately after pairing. Check the system journal for BlueZ profile errors.

journalctl -xeu bluetooth
# Filter the journal for Bluetooth daemon logs with explanatory context

Look for lines mentioning a2dp_sink or headset_head_unit. If the log shows Profile connect failed: Input/output error, the radio firmware is likely outdated or the kernel Bluetooth stack is rejecting the codec. Fedora ships with recent kernel versions, but some older chipsets need a firmware update from the manufacturer.

Audio stuttering or crackling usually points to a codec mismatch. PipeWire negotiates the highest quality codec both devices support. Some cheap earbuds advertise high bitrate support but fail to maintain the buffer. You can force a more stable codec by editing the PipeWire Bluetooth configuration. Never edit files in /usr/lib/. Copy the configuration to /etc/ and modify the copy.

sudo cp /usr/share/pipewire/pipewire.conf.d/92-low-latency.conf /etc/pipewire/pipewire.conf.d/
# Preserve the upstream package files and create a local override directory

Restart PipeWire to apply changes.

systemctl --user restart pipewire pipewire-pulse
# Reload the media session manager and PulseAudio compatibility layer

Convention aside: Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/. Package updates will overwrite /usr/lib/ and erase your changes.

Read the journal before you disable the radio. The error message usually names the exact profile that failed.

When to use the terminal versus the desktop settings

Use the desktop sound settings when you only need to switch between two known devices and want visual feedback. Use bluetoothctl when the GUI shows a device as connected but audio refuses to route. Use pactl or wpctl when you need to force a specific codec or debug sink routing. Use journalctl -xeu bluetooth when the radio drops connections or fails to discover new hardware. Stick to the GUI for daily use. Drop to the terminal when the abstraction layer hides the actual failure point.

Trust the package manager. Manual file edits drift, snapshots stay.

Where to go next