Hero image

filemtime asset versioning in WordPress themes

The Short Version

In a WordPress theme, add filemtime() to the wp_enqueue_style() and wp_enqueue_script() calls; the returned timestamp becomes the version query string, ensuring browsers fetch the latest CSS or JS after each deployment while still allowing long cache‑control headers.

Why asset versioning is required for long cache headers

When you set Cache‑Control: max‑age=31536000 on static assets, browsers keep the file for a year. If the file changes but the URL stays the same, the browser continues to serve the stale copy. Adding a version parameter (e.g. style.css?ver=1657891234) forces a new request whenever the underlying file is modified. For the response-header side of caching, see our explanation of Cache-Control on 304 responses.

Using filemtime() in a theme

WordPress already accepts a version argument in its enqueue functions. By passing the file’s modification timestamp, the version updates automatically whenever the file is saved. Asset versioning belongs in the deployment process for a custom WordPress build, so every release delivers the matching CSS and JavaScript.

// functions.php – WordPress 6.5 or later
function mytheme_enqueue_assets() {
    // CSS
    $css_path = get_template_directory() . '/assets/css/main.css';
    wp_enqueue_style(
        'mytheme-main',
        get_template_directory_uri() . '/assets/css/main.css',
        array(),
        file_exists( $css_path ) ? filemtime( $css_path ) : false
    );

    // JavaScript
    $js_path = get_template_directory() . '/assets/js/bundle.js';
    wp_enqueue_script(
        'mytheme-bundle',
        get_template_directory_uri() . '/assets/js/bundle.js',
        array(),
        file_exists( $js_path ) ? filemtime( $js_path ) : false,
        true
    );
}
add_action( 'wp_enqueue_scripts', 'mytheme_enqueue_assets' );

The file_exists() guard prevents a PHP warning if the file is missing during a partial build. The version argument can be false – WordPress will then omit the query string, which is acceptable for assets that never change (e.g. third‑party libraries).

Common pitfalls and how to avoid them

  • Incorrect path. get_template_directory() returns the absolute server path; using get_stylesheet_directory() is required for child themes.
  • Cache busting during development. When WP_DEBUG is true, you may prefer time() to force a fresh version on every page load, avoiding the need to clear the browser cache.
  • File permission issues. If the web server cannot read the file’s metadata, filemtime() returns false. Ensure the assets are readable by the PHP process.

Quick tip

Wrap the enqueue logic in a helper function if you have many assets; this keeps the functions.php file tidy and makes future changes easier.

Manual version strings vs. filemtime()

Both approaches achieve cache busting, but they differ in maintenance overhead.

Manual version strings

  • Developer writes the version number.
  • Easy to set a specific version for a release.
  • Requires a code change for every asset update.
  • Risk of forgetting to bump the number, leading to stale assets.

filemtime() versioning

  • Version updates automatically with file changes.
  • No extra step in the deployment workflow.
  • Works for any file that is edited locally.
  • Cannot be used for remote CDN assets that are not on the same server.

Integrating with a CDN

If you serve assets from a CDN, you still need a version string in the URL. After enqueueing with filemtime(), you can prepend the CDN domain using a filter:

// functions.php – WordPress 6.5+
function mytheme_cdn_url( $src, $handle ) {
    if ( strpos( $src, '/assets/' ) !== false ) {
        $src = str_replace( get_template_directory_uri(), 'https://cdn.example.com/theme', $src );
    }
    return $src;
}
add_filter( 'style_loader_src', 'mytheme_cdn_url', 10, 2 );
add_filter( 'script_loader_src', 'mytheme_cdn_url', 10, 2 );

The filter swaps the origin of any URL that contains /assets/ with the CDN hostname while preserving the version query string generated by filemtime(). This keeps the cache‑busting behaviour intact across the CDN edge.

Testing that versioning works

  1. Open the site in a browser and view the page source.
  2. Locate a stylesheet or script tag; note the ver= value.
  3. Modify the corresponding file (e.g. add a comment).
  4. Refresh the page and confirm the ver= value has changed.
  5. Inspect the network panel; the asset should be fetched with a 200 status, not served from cache.

Performance impact

Calling filemtime() for each asset adds a single filesystem stat per request. On a typical theme with a handful of assets this overhead is negligible (well under a millisecond). If you have dozens of assets, consider caching the timestamps in a transient:

// functions.php – optional optimisation
function mytheme_get_asset_version( $relative_path ) {
    $cache_key = 'mytheme_version_' . md5( $relative_path );
    $version   = get_transient( $cache_key );

    if ( false === $version ) {
        $full_path = get_template_directory() . $relative_path;
        $version   = file_exists( $full_path ) ? filemtime( $full_path ) : false;
        set_transient( $cache_key, $version, DAY_IN_SECONDS );
    }

    return $version;
}

Replace the direct filemtime() calls with mytheme_get_asset_version() to avoid repeated stat calls during a high‑traffic period.

 

Key takeaway

Using filemtime() for asset versioning gives you automatic cache busting with zero manual steps, allowing you to set long‑term cache headers confidently; combine it with a CDN filter to keep the same workflow when assets are served from an external host.

Need help putting this into practice?

Explore our WordPress Web Design, or tell us about your project so we can discuss the next step.

Illustration of a LibraFire team member on a video call with a client

How can we assist you?

Contact Us