9 min read
How to Limit Audio Plays in WordPress
You can limit how many times an audio file plays on a WordPress site, but how well the limit holds depends entirely on where you count the plays. Count in the browser and a determined visitor resets it in seconds. Count on the server, per logged-in user, and the limit holds for anyone who does not have the raw file URL. Protect the file URL too and you have something that works for paid previews, listening tests, language exams and limited-play samples.
This guide covers all three levels: a free plugin for the simple case, a custom solution with server-side counting for members, and the file-protection step that makes either one meaningful. Code is included and tested against WordPress 7.0.
Decide what you are actually limiting

Before installing anything, be clear about which of these you need, because they call for different tools:
- Play once per visit. A pronunciation test, a one-shot listening exercise, an audio message that should not be replayed. Browser storage is fine.
- N plays per user, forever. “Three free listens before you buy”, exam audio with a fixed replay allowance. Needs a logged-in user and a server-side counter.
- A preview clip, unlimited plays. Sell music or audiobooks and let people hear 30 seconds. This is not a play limit at all; it is a preview, and it is covered at the end.
- Stop downloads. Any of the above is pointless if the visitor can open the MP3 URL in a new tab. File protection is a separate layer and you almost always need it.
One honest caveat up front. Audio delivered to a browser can always be captured by recording system output. What you can prevent is casual replay, sharing of the direct file link, and unlimited listening by people who have not paid. That is enough for most use cases.
Option 1: the Play Audio Once plugin (per visit, no login)
Play Audio Once on WordPress.org does one job: it lets an audio file play a single time per browser session, either site-wide or for files you mark in the Block Editor or Elementor. It stores the “played” flag in the browser’s session storage, not a cookie, and it records the flag the moment playback starts, so the listener cannot scrub backwards either. It was updated in August 2026 and is tested up to WordPress 7.1.
Setup takes two minutes:
- Install and activate from Plugins → Add New.
- Go to Settings → Play Audio Once and choose whether the limit applies to every audio element on the site or only to blocks where you enable it.
- For per-file control, select the Audio block in the editor and tick the play-once option in the block sidebar.
Limitations, so you pick it with open eyes: session storage clears when the tab closes, so “once” means “once per tab session”. There is no per-user memory, no admin reset, and JavaScript must be enabled. It also does nothing to hide the file URL. For a classroom listening exercise it is ideal. For anything involving money it is not enough on its own.
Option 2: a custom per-user play limit with server-side counting

For a real limit you need three parts: a counter stored against the user, a small REST endpoint that increments it, and a front-end script that checks the count before allowing play. The code below is a compact, working version you can drop into a site-specific plugin. It limits each logged-in user to a configurable number of plays per attachment.
Step 1: the shortcode and the counter
define( 'WBC_AUDIO_LIMIT', 3 );
function wbc_audio_plays( $user_id, $attachment_id ) {
return (int) get_user_meta( $user_id, 'wbc_plays_' . $attachment_id, true );
}
add_shortcode( 'limited_audio', function ( $atts ) {
$atts = shortcode_atts( array( 'id' => 0 ), $atts );
$id = absint( $atts['id'] );
if ( ! $id || ! is_user_logged_in() ) {
return '<p>Please log in to listen.</p>';
}
$left = WBC_AUDIO_LIMIT - wbc_audio_plays( get_current_user_id(), $id );
if ( $left <= 0 ) {
return '<p>You have used all your plays for this track.</p>';
}
wp_enqueue_script( 'wbc-limited-audio', plugins_url( 'limited-audio.js', __FILE__ ), array(), '1.0', array( 'strategy' => 'defer' ) );
wp_localize_script( 'wbc-limited-audio', 'wbcAudio', array(
'rest' => esc_url_raw( rest_url( 'wbc/v1/play' ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
) );
return sprintf(
'<audio class="wbc-limited" data-id="%1$d" controls controlsList="nodownload" preload="none" src="%2$s"></audio><p class="wbc-left">Plays left: %3$d</p>',
$id,
esc_url( add_query_arg( array( 'id' => $id, 'wbc_token' => wp_create_nonce( 'wbc_stream_' . $id ) ), home_url( '/wbc-stream/' ) ) ),
$left
);
} );
Step 2: the REST endpoint that records a play
add_action( 'rest_api_init', function () {
register_rest_route( 'wbc/v1', '/play', array(
'methods' => WP_REST_Server::CREATABLE,
'permission_callback' => 'is_user_logged_in',
'args' => array( 'id' => array( 'sanitize_callback' => 'absint', 'required' => true ) ),
'callback' => function ( WP_REST_Request $req ) {
$user = get_current_user_id();
$id = $req['id'];
$plays = wbc_audio_plays( $user, $id );
if ( $plays >= WBC_AUDIO_LIMIT ) {
return new WP_Error( 'limit', 'Play limit reached', array( 'status' => 403 ) );
}
update_user_meta( $user, 'wbc_plays_' . $id, $plays + 1 );
return array( 'left' => WBC_AUDIO_LIMIT - $plays - 1 );
},
) );
} );
Step 3: the front-end script
document.querySelectorAll( 'audio.wbc-limited' ).forEach( ( el ) => {
let counted = false;
el.addEventListener( 'play', async ( e ) => {
if ( counted ) return;
el.pause();
const res = await fetch( wbcAudio.rest, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': wbcAudio.nonce },
body: JSON.stringify( { id: el.dataset.id } ),
} );
if ( ! res.ok ) {
el.replaceWith( Object.assign( document.createElement( 'p' ), { textContent: 'Play limit reached.' } ) );
return;
}
const data = await res.json();
counted = true;
el.nextElementSibling.textContent = 'Plays left: ' + data.left;
el.play();
} );
} );
The script pauses on the first play event, asks the server for permission, and only resumes if a play was recorded. Because the count is in user meta, it survives cache clears, new devices and private windows, and an admin can reset it by deleting the meta key (or with wp user meta delete 42 wbc_plays_123).
Two refinements worth adding in production: count a play only after the ended event or after a percentage has been heard, so a mis-click does not cost the listener a play, and store a timestamp alongside the count if you want the allowance to reset monthly.
Protect the file URL, or the limit means nothing
Notice that the shortcode above does not point at /wp-content/uploads/track.mp3. It points at a streaming endpoint with a nonce. Without this, the limit is cosmetic: anyone can copy the source URL from dev tools and play it forever. The minimal version of that endpoint:
add_action( 'init', function () {
add_rewrite_rule( '^wbc-stream/?
WBComDesigns is amazing!
WBComDesigns is amazing. The WP plugins and themes are of top notch quality with lots of features and updated very frequently. Supports are very responsive and helpful. I use Reign with TutorLMS, and the experience is surprisingly smooth.…