9 min read

How to Limit Audio Plays in WordPress

Shashank Dubey
Content & Marketing, Wbcom Designs · Published Aug 7, 2024 · Updated Aug 29, 2026
WordPress Experts by Wbcom Designs - galaxy background with handwriting text

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

Card showing three ways to limit audio plays in WordPress: once per visit, N plays per user, and preview clips

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:

  1. Install and activate from Plugins → Add New.
  2. 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.
  3. 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

Four steps to limit audio plays in WordPress with a server-side counter, REST endpoint, JS gate and protected file URL

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/?
 
, 'index.php?wbc_stream=1', 'top' ); } ); add_filter( 'query_vars', fn( $v ) => array_merge( $v, array( 'wbc_stream' ) ) ); add_action( 'template_redirect', function () { if ( ! get_query_var( 'wbc_stream' ) ) return; $id = absint( $_GET['id'] ?? 0 ); $token = sanitize_text_field( wp_unslash( $_GET['wbc_token'] ?? '' ) ); if ( ! is_user_logged_in() || ! wp_verify_nonce( $token, 'wbc_stream_' . $id ) ) { status_header( 403 ); exit; } if ( wbc_audio_plays( get_current_user_id(), $id ) > WBC_AUDIO_LIMIT ) { status_header( 403 ); exit; } $path = get_attached_file( $id ); header( 'Content-Type: ' . get_post_mime_type( $id ) ); header( 'Content-Length: ' . filesize( $path ) ); header( 'Cache-Control: no-store' ); readfile( $path ); exit; } );

Move the actual audio files outside the public uploads folder (or block direct access with a rule in the uploads .htaccess or your Nginx config) so the only way to get bytes is through this endpoint. Nonces expire after 24 hours by default, which is a useful side effect: a copied stream URL stops working the next day. For larger files, add HTTP range support so scrubbing works, or serve through a CDN with signed URLs (Bunny Stream and Cloudflare R2 both support token authentication) instead of readfile().

Comparing the approaches

Table comparing approaches to limit audio plays in WordPress: Play Audio Once plugin, custom per-user code, protected media plugin
Play Audio OnceCustom per-user limitProtected media plugin
Where plays are countedBrowser session storageUser meta on the serverServer, with logging
Survives new tab / deviceNoYesYes
Works for guestsYesNo (needs login)Usually via email gate
Hides the file URLNoYes, with the stream endpointYes (signed URLs, optional DRM)
Admin resetNoDelete user metaDashboard
Effort2 minutes1 to 2 hours30 minutes
CostFreeDeveloper timeFree core, paid Pro

If you would rather not maintain code, a protected media plugin gives you the server-side half ready made. Our MediaShield plugin serves self-hosted and Bunny-hosted media through a protected player, blocks direct links and right-click downloads, caps concurrent streams per account and records who played what. The Pro tier adds ClearKey and Bunny DRM, playback heatmaps and LMS completion adapters, which is what course sites use to mark a lesson done only after the audio has been heard. It is video-first but handles audio files the same way.

If you sell audio, you probably want a preview instead

Stores that asked us for “play limits” usually wanted a free sample before purchase, which is a different and simpler problem. For WooCommerce, Audio Preview for WooCommerce adds a player to the product page and shop grid and cuts playback at a length you set (say 30 seconds), while the full file stays a protected downloadable that only buyers receive. The Pro version removes the preview limits and adds waveform players and file protection. Unlimited replays of a short clip sell more than three plays of the full track, and you avoid the support tickets from people who used their plays and now cannot hear what they are buying.

Frequently asked questions

Can I limit plays for visitors who are not logged in?

Only loosely. You can use a cookie or local storage (the Play Audio Once approach), or key the count to an IP address, which punishes shared offices and resets for anyone on mobile data. If the limit matters, require a free account. BuddyPress, BuddyNext and every membership plugin make registration painless, and you gain an email address for follow-up.

Does this work with the core Audio block?

The plugin route does. The custom code above uses a shortcode for clarity, but you can apply the same script to the core block by filtering render_block_core/audio to add the wbc-limited class and swap the src for the protected endpoint.

Will caching plugins break the counter?

Not if the count is checked over REST at play time, as in the code above. What caching does break is printing “Plays left: 2” into a cached page, so either exclude those pages from the page cache (logged-in pages usually are) or fetch the remaining count with the same REST call on load.

Can users still record the audio?

Yes. Nothing delivered to a browser is immune to screen or audio recording. The goal is to stop link sharing and unlimited listening, not to defeat a microphone held up to a speaker.

Where to start

For a quiz or one-off listening exercise, install Play Audio Once and stop there. For members who get a fixed number of listens, use the user-meta counter and the protected stream endpoint together; neither is useful alone. For a course or media library with many files and a need for reporting, put MediaShield or a similar protected player in front of the files rather than maintaining custom code. And if the real question was “how do I let people sample before buying”, skip limits entirely and give them an unlimited short preview.

Related reading