Propshaft is Rails 8’s default asset pipeline. It’s simpler than what came before; basically, it does two things: fingerprints your files with SHA-256 digests and serves them. It’s deliberately simple; no minification, no bundling, no Babel transpilation. (It does, however, rewrite paths in CSS and JS to use the aforementioned fingerprinted paths, so no special helpers are needed.)
The Rails asset pipeline has a long history. Sprockets shipped with Rails 3.1 in 2011, and for years it handled nearly everything: concatenation, minification, SCSS compilation, CoffeeScript transpilation, ERB processing, and cache-busting fingerprints. It was thorough, and it was heavy.
The web has changed considerably since then. HTTP/2 multiplexes multiple requests over a single connection, removing much (though not all) of the pressure to concatenate files into bundles.
Likewise, modern browsers support ES6 modules natively. Build tools like esbuild and Vite handle transpilation faster than any in-process Ruby solution can. Against that backdrop, most of what Sprockets does is no longer necessary - and what remains necessary, it does slowly.
Propshaft is the Rails team’s answer to that mismatch. It was available as a manual option from Rails 7.1 onward, became the default in Rails 8, and was highlighted alongside Kamal 2 and Thruster at Rails World 2024 as part of a broader push to simplify the Rails deployment story. This guide covers how Propshaft works, how it compares to Sprockets, how to migrate an existing application, and how to deploy with proper caching. We also discuss when Propshaft is the right choice and when keeping Sprockets makes more sense.
How Propshaft Works
Before getting into migration steps, it helps to understand Propshaft’s model - because it is fundamentally different from Sprockets, and that difference shapes every decision downstream.
When you run rails assets:precompile, Propshaft does the following:
- Scans the asset load path (
app/assets,lib/assets,vendor/assets, and any paths you add). - Computes a SHA-256 digest of each file’s contents.
- Copies each file to
public/assetswith its digest appended to the filename. - Writes a
manifest.jsonmapping original names to digested names.
The manifest looks like this:
{
"application.js": "application-2d7bd4a774e15b24d7fdda3fe4225884.js",
"logo.png": "logo-3bce1b2f32c5e0abc4b550e965bb03d8.png",
"app.css": "app-efgh5678.css"
}Rails view helpers read this manifest at runtime to generate the correct URLs:
<%= image_tag "logo.png" %>
<!-- renders: <img src="/assets/logo-3bce1b2f32c5e0abc4b550e965bb03d8.png" /> -->In CSS, relative url() references are rewritten to their digested equivalents automatically. For JavaScript, if you need to reference an asset path dynamically, Rails.assetPath("icon.svg") returns the correct digested path.
To load assets in your layout:
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>Notice there are no Sprockets-style //= require directives. Propshaft does not process or concatenate files. Each file you reference is served as-is, with only its filename altered to include the digest.
In development, assets are served directly from the load path without precompilation. In production, rails assets:precompile runs once during the build step, and the resulting files in public/assets are served statically.
The “No Build” Philosophy
Propshaft is designed around Rails 8’s “No Build” stance. The idea is that modern browsers support ES6 modules natively, HTTP/2 handles multiple concurrent requests efficiently, and the overhead of a Node.js build step is no longer justified for applications that do not need it.
Propshaft provides load paths and digests; everything else is explicitly out of scope. You might think of this as a limitation, but it is a design decision that keeps the pipeline fast, auditable, and small.
For applications that do need bundling or transpilation, those steps happen outside Propshaft via dedicated tools, and Propshaft digests their output. The two concerns are cleanly separated.
Note that “No Build”, as such, is not a Durable Programming philosophy; in fact, we highly support build phases when they speed up end-user experiences. However, Propshaft in no way prevents you from using other build tools, and it sticks to a limited set of tasks that it can do effectively.
Cache Headers Out of the Box
Propshaft sets Cache-Control: public, max-age=31536000, immutable on digested assets automatically in production - you do not need to configure this in your web server if you are running Thruster or a similar Rack middleware that handles asset serving. The immutable directive tells browsers not to revalidate the file even on an explicit refresh, since the filename itself is the version identifier. This is a meaningful improvement over Sprockets, which required explicit config.assets.cache_control configuration to achieve the same result.
Subresource Integrity
Because Propshaft uses SHA-256 digests, those same hashes can be used directly in Subresource Integrity (SRI) attributes. SRI allows browsers to verify that a fetched file has not been tampered with - the browser computes the file’s hash and compares it against the value in the integrity attribute before executing or applying the resource. If your application serves assets from a CDN or third-party host and you want this protection, Propshaft’s SHA-256 algorithm is already the right format; MD5, which Sprockets used, is not accepted for SRI.
Propshaft vs. Sprockets
The table below summarizes the main differences:
| Feature | Propshaft | Sprockets |
|---|---|---|
| Default in | Rails 8 (optional from 7.1) | Rails 3.1 through 7.2 |
| Digest algorithm | SHA-256 | MD5 |
| File processing | None - copy only | SCSS, CoffeeScript, ERB, and more |
| Bundling/concatenation | None | Yes, via //= require directives |
| Cache-Control | Set automatically (immutable) | Requires explicit configuration |
| Memory during precompile | ~1.2 GB (benchmark) | ~1.8 GB (benchmark) |
| Precompile time | ~19% faster on equivalent asset sets | Baseline |
| Runtime serving latency | ~9 ms (benchmark) | ~12 ms (benchmark) |
| Manifest format | manifest.json | manifest.yml |
| Configuration surface | Minimal | Extensive |
| Security | No code execution in processors | Custom processors can execute Ruby |
| SRI compatibility | Yes (SHA-256) | No (MD5) |
There are a few points worth unpacking here.
On processor support: Sprockets’ processor pipeline is what makes it capable of compiling SCSS, transpiling CoffeeScript, and evaluating ERB in asset files. Propshaft has none of that. If your application depends on //= require directives or SCSS compilation through the asset pipeline, those do not work with Propshaft out of the box. Compilation must be delegated to external tools before Propshaft sees the files.
On digest algorithm: Propshaft uses SHA-256 where Sprockets used MD5. SHA-256 is cryptographically stronger, which matters if your digest values are used in any security-sensitive context - for example, verifying asset integrity with Subresource Integrity (SRI) headers.
On security: Removing arbitrary processor execution reduces the attack surface for supply-chain compromises. A malicious gem that hooks into Sprockets’ processor chain can execute Ruby during precompilation; Propshaft offers no such hook.
On manifest format: Sprockets uses manifest.yml; Propshaft uses manifest.json. This matters if you have scripts or deployment tooling that reads the manifest directly.
On configuration: Sprockets has accumulated a large number of config.assets.* settings over the years. Propshaft’s configuration is much smaller. The main settings you will use are:
# config/application.rb
config.assets.paths << Rails.root.join("app/assets/fonts")
# config/environments/production.rb
config.assets.digest = true
config.assets.compile = false # Required — never compile on-the-fly in production
config.asset_host = ENV["CDN_HOST"]config.assets.compile = false is important to set explicitly: on-the-fly compilation in production is discouraged and should be disabled. With Propshaft, precompilation is always expected to happen during the build step.
The evolution of Rails asset handling looks roughly like this:
| Rails version | Default asset approach |
|---|---|
| Rails 6 | Sprockets, with Webpacker for JavaScript |
| Rails 7 | Sprockets default; Propshaft available manually; Import Maps introduced |
| Rails 7.1 | Propshaft available as gem option |
| Rails 8 | Propshaft default |
Pairing Propshaft with JavaScript Bundlers
As noted above, Propshaft does not bundle or transpile JavaScript. The Rails 8 default assumes you are either using Import Maps (which serve ES modules directly to modern browsers) or a dedicated build tool that handles bundling before Propshaft sees the output.
There are three main approaches:
Import Maps (the Rails 8 default). Import Maps let the browser load ES modules directly from the server. No bundling is required. Propshaft digests the files, and the browser resolves module paths from the import map. This works well for applications that do not depend on complex npm packages.
One trade-off worth noting: integrating npm packages that ship as CommonJS or that require build-time tree-shaking can be awkward with the vanilla Propshaft + importmap-rails setup. For applications with a significant npm dependency footprint, a bundler is the cleaner path.
esbuild or Vite via jsbundling-rails or vite_rails. These tools run a separate build step that outputs bundled, transpiled JavaScript into app/assets/builds. Propshaft then digests those output files normally. The jsbundling-rails gem coordinates this and works with either Propshaft or Sprockets as the downstream pipeline.
# esbuild outputs to app/assets/builds/
# Propshaft digests everything in app/assets/
# No conflict between the two stepsTailwind CSS. The tailwindcss-rails gem runs Tailwind’s CLI and outputs a compiled CSS file to app/assets/builds/application.css. Propshaft picks it up from there. No SCSS pipeline is needed.
The key principle: Propshaft handles versioning and serving; everything else is your responsibility to wire up before that step.
Handling Sass with dartsass-rails
For applications that use Sass, the dartsass-rails gem fills the gap that Propshaft deliberately leaves open. It runs the Dart Sass CLI as a build step and writes compiled CSS output to app/assets/builds. Propshaft then picks up those compiled files and digests them normally - it never sees .scss files at all.
As an aside, note that the Sass project supports both the older Sass and the newer scss - so if you have .sass files, dartsass will work, or if you have .scss files, that will work too.
# Gemfile
gem "dartsass-rails"bundle install
rails dartsass:installAfter installation, dartsass-rails adds a watcher process that recompiles Sass on change during development. In production, Sass compiles as part of your asset precompile step. The important point is that this is an external compilation step - completely separate from Propshaft’s responsibility.
Migrating from Sprockets
Before we get into the steps, a note on scope. Migration is straightforward for applications that use Sprockets mainly for fingerprinting and serving static files. It is more involved for applications that lean heavily on Sprockets processors - particularly compiled SCSS, complex //= require trees, or custom processors. For those, gradual migration is often more practical than a single-step switch.
Back up config/initializers/assets.rb and any custom Sprockets processors before you begin.
Step 1: Update Gemfile Dependencies
# Remove from Gemfile:
# gem "sprockets-rails"
# gem "sass-rails"
# Add:
gem "propshaft"Then run:
bundle installClean up configuration files and initializers that Sprockets created:
rm -rf app/assets/config/ config/initializers/assets.rbRemove any Sprockets::Railtie references in your initializers. These are no-ops once Sprockets is removed, but they will raise a NameError if left behind.
Step 2: Update Your Application Layout
Sprockets-style tags:
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track": "reload" %>
<%= javascript_pack_tag "application", "data-turbolinks-track": "reload" %>Propshaft + Import Maps:
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>Note the change from data-turbolinks-track to data-turbo-track - this is the Turbo 8+ convention. After restarting the server, assets will begin serving directly from the load path without a precompile step in development.
Step 3: Handle Preprocessors
Propshaft does not run SCSS or CoffeeScript. If you have Sass files that were compiled by Sprockets, you have two options:
- Migrate to plain CSS. Often the right call for simpler stylesheets that do not rely on complex Sass features like mixins or functions.
- Use
dartsass-rails, which compiles Sass to plain CSS in a separate step before Propshaft sees the output. This is the recommended path for applications with substantial Sass codebases.
If you are not yet ready to migrate preprocessors, you can exclude specific paths from Propshaft while you work through the transition:
# config/environments/development.rb
# config/environments/production.rb
config.assets.excluded_paths << Rails.root.join("app/assets/stylesheets")For esbuild or Vite output, configure those tools to write into app/assets/builds:
# config/environments/production.rb
config.assets.digest = true
config.assets.compile = falsePropshaft will pick up whatever lands in app/assets/builds and digest it normally.
Step 4: Verify
Run a production dry-run to confirm the precompile works:
RAILS_ENV=production bin/rails assets:precompile --dry-runYou should see Propshaft scanning and copying files. Check that public/assets/manifest.json is generated and that fingerprinted filenames appear in your layout helpers.
If you have stale digests from a previous Sprockets precompile, clean them:
bin/rails assets:cleanCommon Pitfalls
//= require directives are ignored. Propshaft does not parse Sprockets manifest directives. If your application.js or application.css relies on //= require to concatenate files, those dependencies will not be included. Use Import Maps, a bundler, or explicit <script> tags instead.
Asset URL helpers work differently. asset_path, image_tag, and similar helpers still work and now delegate to Propshaft. ERB asset helpers in CSS files (e.g., url(<%= asset_path "logo.png" %>)) are not supported by Propshaft - use relative paths in CSS instead, and Propshaft will rewrite them automatically.
Custom Sprockets processors have no equivalent. Gems or initializers that hook into Sprockets’ processor pipeline will need to be replaced with external build tools. There is no Propshaft processor API.
Stale manifests from Sprockets. After switching, run bin/rails assets:clean to remove old Sprockets-generated files. Stale digests from a previous pipeline can cause mismatches in helper output.
Sprockets::Railtie references. Any initializer or gem that references Sprockets::Railtie will raise a NameError once Sprockets is removed. Search your codebase for these before removing the gem:
grep -r "Sprockets::Railtie" app/ config/ lib/npm packages that require CommonJS bundling. The Import Maps + Propshaft combination works cleanly for ES module packages but requires workarounds for packages that ship as CommonJS or need tree-shaking. If this describes your npm dependencies, use jsbundling-rails with esbuild or Vite rather than fighting the default setup.
Deploying with CDN and Cache Headers
Asset digests are the foundation of far-future caching. Because the filename changes whenever the file content changes, browsers and CDNs can safely cache digested files indefinitely. A file served as logo-abcd1234.png today will never be served with different content under that name.
Precompile first, then deploy:
RAILS_ENV=production rails assets:precompileThis populates public/assets with digested files and the manifest.
Configuring a CDN
In config/environments/production.rb:
config.asset_host = ENV["CDN_HOST"]For protocol-relative URLs:
config.asset_host = "//#{ENV["CDN_HOST"]}"For more control - for example, routing only asset paths through the CDN:
config.asset_host = lambda do |source|
"//#{ENV["CDN_HOST"]}" if source.start_with?("/assets/")
endVerify by inspecting the HTML: you should see <img src="//cdn.example.com/logo-abcd1234.png">.
NGINX Cache Configuration
If you are not using Thruster (which handles asset serving and cache headers automatically), configure NGINX to serve digested assets with far-future expiry and the immutable directive:
server {
listen 80;
server_name yourapp.com;
location ~ ^/assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
try_files $uri =404;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
}The immutable directive tells browsers not to revalidate even on explicit refresh - a meaningful performance improvement for assets that will never change under a given URL. Verify with:
curl -I https://yourapp.com/assets/logo-abcd1234.png
# Should include: Cache-Control: public, immutable, max-age=31536000This setup enables zero-downtime deploys: old and new asset versions can coexist in public/assets because each deploy generates new filenames. The assets:clean task prunes old versions once you are confident the deploy succeeded.
Thruster Integration
Thruster is the HTTP/2 proxy introduced alongside Propshaft in Rails 8. When you deploy with Kamal 2 and Thruster in the default configuration, Thruster automatically handles asset serving with proper cache headers - you do not need to configure NGINX or another reverse proxy specifically for /assets/. This is part of the cohesive deployment story that Rails 8 was designed around: Propshaft produces versioned files, Thruster serves them with correct caching, and Kamal 2 handles the deployment orchestration.
Thruster also handles X-Sendfile acceleration for file downloads and HTTP/2 push hints, making it a natural fit for the Propshaft model where assets are static, digested files rather than dynamically compiled output.
Trade-offs and When to Stick with Sprockets
Propshaft is not the right choice in every situation. The case for migrating is strongest when:
- Precompile times are causing CI timeouts or noticeably slowing deploys.
- You are starting a new Rails 8 application and do not have legacy asset requirements.
- You are already using Import Maps or a dedicated bundler for JavaScript.
- You want a smaller configuration surface and fewer moving parts in production.
The case for staying with Sprockets, at least initially, is strongest when:
- Your application relies heavily on SCSS compilation through the asset pipeline.
- You have complex
//= requiredirective trees that would require significant refactoring. - You use custom Sprockets processors that have no clear external-tool equivalent.
- You are in the middle of a larger Rails upgrade and want to limit simultaneous changes.
- Your npm dependency footprint relies on CommonJS packages that need bundling.
It is worth noting that Sprockets is not going away. It remains a supported gem and a reasonable choice for applications that depend on its features. The Rails team defaulting to Propshaft in Rails 8 reflects where new application development is heading - not a forced deprecation of existing apps.
For teams considering migration, a practical approach is to run a production dry-run first, observe what fails, and scope the migration work from there. Some applications migrate in an hour; others require several days of careful refactoring.
Frequently Asked Questions
What does Propshaft actually do? Propshaft provides a load path for assets and appends SHA-256 digests to filenames for cache busting. It copies files to public/assets and writes a manifest.json that Rails helpers use to resolve the correct URL. That is the full scope of its responsibilities - no compilation, no concatenation, no processing pipeline.
Does Propshaft replace Webpacker? No. Webpacker was a separate tool for bundling JavaScript via Webpack, which was deprecated in Rails 7. Propshaft replaces Sprockets as the asset pipeline responsible for serving and fingerprinting files. If you need bundling, pair Propshaft with esbuild or Vite via jsbundling-rails.
Can I use Sass with Propshaft? Not directly - Propshaft does not compile Sass. Use the dartsass-rails gem to compile Sass to plain CSS first; Propshaft then picks up the compiled output and digests it. The two tools work well together and do not conflict.
Is Propshaft required for Rails 8? No. Sprockets can still be used in Rails 8 by adding it to your Gemfile. Propshaft is the new default for generated apps, not a hard requirement.
Why is Propshaft faster than Sprockets? Sprockets builds a processor chain for each asset, potentially executing SCSS compilation, CoffeeScript transpilation, ERB evaluation, and concatenation. Propshaft copies files and hashes them - no processing pipeline means dramatically less memory and CPU usage during precompilation. Benchmarks show approximately 19% faster precompile times and around 33% lower peak memory usage.
What happened to //= require directives? Propshaft ignores them. They are a Sprockets feature for bundling files together. With Propshaft, use Import Maps to load ES modules directly, or use a bundler like esbuild to produce a single output file that Propshaft then digests.
Does Propshaft work with Import Maps? Yes. This is the recommended Rails 8 setup. Import Maps handle module resolution in the browser; Propshaft handles fingerprinting. They complement each other without overlap. The main limitation is packages that ship as CommonJS or require tree-shaking - those are better served by a bundler.
How does Propshaft handle cache headers? In production, Propshaft sets Cache-Control: public, max-age=31536000, immutable on digested assets automatically. The immutable directive tells browsers not to revalidate the file even on a hard refresh, which is safe because digested filenames are unique to their content. If you are serving assets through Thruster, this behavior is built in without additional web server configuration.
What is the difference between manifest.json and .manifest.json? Propshaft writes a file named manifest.json in public/assets. Older references in some documentation use .manifest.json (with a leading dot), but the current gem uses the non-hidden name. Verify by running bin/rails assets:precompile and checking public/assets/ directly.
Can I use Propshaft with Vite? Yes, via the vite_rails gem. Vite handles bundling and hot module replacement in development, and outputs production-ready files that Propshaft digests. Vite’s own manifest handles module resolution; Propshaft handles cache-busting fingerprints. Configure Vite to output to a directory on Propshaft’s load path.
What happens during a zero-downtime deploy? Because every asset filename includes a content-based digest, old and new asset versions coexist in public/assets during and after a deploy. Requests that arrive while the old version of the app is still running will receive the old digested URLs, which still resolve correctly. Once you are confident the deploy succeeded, run bin/rails assets:clean to remove asset versions older than a configurable cutoff (default: keep the two most recent versions).
Resources:

