activerecord-session_store: keeping Rails sessions in a database table
Active Record's Session Store extracted from Rails
At a glance
- What is it?
- The activerecord-session_store gem moves Rails sessions out of the cookie and into an Active Record table, with a trim task to keep the table bounded, pluggable serializers and a documented upgrade path for the CVE-2019-25025 timing attack. It is a small gem with several sharp edges around size limits and cleanup.
- Who is it for?
- activerecord-session_store suits a Rails application that needs server side sessions it can query, expire and inspect, and that has a database it is willing to carry the load of. It does not suit an application whose session data is small and whose sessions are better kept in signed cookies, and it does not suit a team unwilling to run a daily cleanup, since the sessions table grows without limit otherwise.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 101 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A session store is a table with two columns
activerecord-session_store is described as Active Record's session store, extracted from Rails. Its whole job is to put session data in a database table instead of in the cookie.
The design is smaller than you might expect. A default class is provided, but the documentation is explicit that any object duck-typing to an Active Record session class with text `session_id` and `data` attributes is sufficient. There is no session interface to implement beyond those attributes for the default behaviour, which is why the gem can be swapped for a custom store later without much ceremony.
The default table has three columns. An `id` as the numeric primary key, a `session_id` string, usually varchar with a maximum length of 255, and a `data` column of type text, longtext, json or jsonb. The `session_id` column should always be indexed for speedy lookups, since every session read is a lookup by that value. The `data` column holds the session payload marshaled to Base64, and the documentation warns to be careful if the session data exceeds 65KB.
That 65KB figure is the first hard constraint worth internalising, because it is not a limit the gem enforces by shrinking anything. If the data written is larger than the column's size limit, `ActionController::SessionOverflowError` is raised, and the failure appears at the moment a user triggers whatever put the oversized value in the session rather than at deploy time.
Three commands to get from Gemfile to a working store
The installation path is short and entirely conventional Rails. The gem goes into the Gemfile:
gem 'activerecord-session_store'Then a migration generator creates the table, and the migration is run:
rails generate active_record:session_migration
rake db:migrateFinally the session store is set in `config/initializers/session_store.rb`:
Rails.application.config.session_store :active_record_store, :key => '_my_app_session'There is one option on that line that deserves attention, and it is a security switch rather than a convenience. Providing `secure_session_only` disables the fallback to an insecure session, so instead of silently degrading to a cookie when the database is unavailable, the store refuses:
Rails.application.config.session_store :active_record_store, key: '_my_app_session', secure_session_only: trueThe difference between the two is what happens during an outage or a failover. Without the flag, a store that cannot reach the database can fall back to the insecure session and keep serving requests. With it, those requests fail instead. Which of the two behaviours a team wants is a decision about whether an outage should degrade or stop, and the option exists because the default is the forgiving one.
The table name, primary key, data column and serializer type are all configurable, by convention at the end of `config/application.rb`:
ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table'
ActiveRecord::SessionStore::Session.primary_key = 'session_id'
ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
ActiveRecord::SessionStore::Session.serializer = :jsonThe sessions table grows forever unless you trim it
The most operationally important thing in the README is a warning about the sessions table expanding without limit, because it stores expired and potentially sensitive session data. The recommended remedy is to schedule the `db:sessions:trim` rake task to run daily in production, and running it deletes all sessions that have not been updated in the last 30 days. The cutoff is adjustable through the `SESSION_DAYS_TRIM_THRESHOLD` environment variable.
Two things follow from that. The first is that this gem is not a drop-in that works and is then forgotten. A scheduled job has to exist, has to be monitored, and has to be adjusted if the session lifetime and the retention window need to differ. A team that copies the installation steps and stops will accumulate rows indefinitely, along with the session payloads inside them.
The second is that the trim window and the session lifetime are separate numbers that people conflate. A 30 day default retention with sessions that expire in a day means the table holds a month of dead sessions. Setting `SESSION_DAYS_TRIM_THRESHOLD` closer to the actual expiry keeps the table proportional to the live session population.
The documentation also notes a small convenience that is worth knowing about: since the default class is a plain Active Record model, adding `created_at` and `updated_at` datetime columns to the sessions table gives you timestamps for free, and with them periodic session expiration becomes a matter of querying by timestamp rather than maintaining a separate expiry column.
Serializers, and what happens when the payload is too large
The serializer controls how the session hash becomes bytes in the `data` column, and there are four named choices plus the option to supply your own class.
`marshal` is the default and uses the built-in Marshal methods coupled with Base64 encoding. `json` does what the name says, using the `parse()` and `generate()` methods of the JSON module. `hybrid` will read either type but write as JSON, which is the useful one when you are migrating an existing table that was written with Marshal. `null` performs no serialization at all and leaves that to the Active Record database adapter, so the database's own JSON support handles the payload, which is the option to pick when the column is jsonb and you want the database doing the work.
You may also provide a class that responds to `#load(value)` and `#dump(value)`, which is the hook for anything else.
The size limit is the part that bites. The default column type is a text column, and session data is Base64 encoded, so the encoded payload is larger than the raw data. The documentation warns to be careful if session data exceeds 65KB, and states that if the data written is larger than the column's size limit, `ActionController::SessionOverflowError` will be raised. With a `longtext` column the limit is far higher, with json and jsonb it depends on the database. A team that stores anything sizeable in the session, an uploaded file reference, a cart, a chunk of search results, needs to check the column type and the encoded size rather than assuming the default text column is enough.
This is also the argument for moving large objects out of the session and into a cache or a table, which is a design decision the gem will not make for you.
Replacing the session class with your own
The gem allows you to provide your own session class implementation, whether a feature-packed Active Record or a bare-metal high-performance SQL store, by setting:
ActionDispatch::Session::ActiveRecordStore.session_class = MySessionClassThe contract is a short method list: `self.find_by_session_id(session_id)`, `initialize(hash_of_session_id_and_data, options_hash = {})`, `attr_reader :session_id`, `attr_accessor :data`, `save` and `destroy`. That is small enough to implement against a hand-written SQL backend, and the repository ships an example SqlBypass class, described as a generic SQL session store that can be used as a basis for high-performance database-specific stores.
The rake tasks impose extra requirements on a custom class, and this is where a first implementation usually trips. `db:sessions:clear` depends on `table_name`, `db:sessions:trim` depends on `where` and `delete_all`, and `db:sessions:upgrade` depends on `secure!`. In other words, a custom store that implements only the six methods above will work for reading and writing sessions and will fail when the maintenance tasks run.
There is a related trap in the configuration route. Setting the primary key to `session_id` frees you from having a separate `id` column, but the documentation is emphatic that you must then set `session.model.id = session.session_id` by hand, and suggests a before filter on ApplicationController as a good place to do it. That is the kind of detail that produces a puzzling failure when the primary key and the model id disagree.
A related logging requirement sits in the same area of the documentation. If you use a logger other than `ActiveSupport::Logger` or its subclasses, you need to include the silencer module manually:
MyLogger.include ActiveSupport::LoggerSilenceand for Rails 5.2 or older, `MyLogger.include ::LoggerSilence`. The silencer is used to silence the logger and not leak private information into the log, and the documentation calls it required for security reasons.
The CVE-2019-25025 upgrade path
Sessions created by Active Record Session Store version 1.x are affected by CVE-2019-25025, and the documented consequence is that an attacker can perform a timing attack against the session IDs stored in the database. The advisory link is given in the README.
The fix has two parts. The library was fixed in version 2.0.0, and after upgrading, the `db:sessions:upgrade` rake task must be run to upgrade all existing session records in the database to the secured version:
$ rake db:sessions:upgradeThe documentation makes two reassuring statements about that task. It is idempotent and can be run multiple times, and session data of users will remain intact. That matters operationally, because a migration over a live sessions table is the kind of task people are nervous about, and knowing that it can be re-run without corrupting user sessions removes the need for a maintenance window.
The task depends on the `secure!` method, so this is also the point at which a custom session class has to implement more than the six core methods.
For a team still on 1.x, the order of operations is: upgrade the gem, run the task, and confirm that the timing behaviour of session id lookups has changed. The remaining question is what to do with sessions that were created before the upgrade if the task cannot be run immediately, and the documentation does not address that, so the safe assumption is that the upgrade task is part of the release, not a follow-up.
Release pace and where this gem fits
The repository is small and conventional: a `Rakefile`, a `Gemfile`, an `activerecord-session_store.gemspec`, `lib/`, `test/`, a `gemfiles/` directory for testing against dependency versions, a `.devcontainer/`, and the usual governance files with `CONTRIBUTING.md` and `CHANGELOG.md`. The licence is MIT.
The release trail shows three versions published on the same day, 2026-07-13: v2.1.0, v2.2.0 and v2.3.0, and the repository is not archived with the last push on 2026-07-14. Publishing three versions in one afternoon is unusual, and it suggests either a coordinated release across a family of gems or a batch of changes going out together. Either way, the practical advice is to read `CHANGELOG.md` when you bump the version, because a minor bump in this gem is not obviously a no-op.
On fit, the case for this gem is specific. Sessions in a cookie are signed but not secret, they are limited in size, and they cannot be revoked server side without extra work. Putting sessions in a table fixes all three: the data stays on the server, the size limit becomes a column type, and a session can be deleted when a user is signed out or banned.
The cost is a database dependency on the request path. Every request that carries a session now does a read and, on write, an update, and the table needs indexes and a cleanup job. For an application with modest session use and a read-mostly workload, the cookie store is simpler and faster. For an application where sessions must be inspectable, revocable or shared across processes, this gem is the direct route, and the trim task is the price of admission.
Editorial conclusion
activerecord-session_store suits a Rails application that needs server side sessions it can query, expire and inspect, and that has a database it is willing to carry the load of. It does not suit an application whose session data is small and whose sessions are better kept in signed cookies, and it does not suit a team unwilling to run a daily cleanup, since the sessions table grows without limit otherwise. Before deploying, schedule db:sessions:trim with the SESSION_DAYS_TRIM_THRESHOLD you want, index the session_id column, check the data column against the 65KB limit, and run db:sessions:upgrade if any sessions were created before version 2.0.0.
Frequently asked questions
How do I install the activerecord-session_store gem?
Add `gem 'activerecord-session_store'` to the Gemfile, run `rails generate active_record:session_migration` followed by `rake db:migrate`, and set the store in `config/initializers/session_store.rb` with `Rails.application.config.session_store :active_record_store, :key => '_my_app_session'`.
How do I stop the sessions table from growing without limit?
Schedule the `db:sessions:trim` rake task to run daily in production. It deletes sessions not updated in the last 30 days, and that cutoff is changed with the `SESSION_DAYS_TRIM_THRESHOLD` environment variable.
What happens if a session is too large for the data column?
If the data written is larger than the column's size limit, `ActionController::SessionOverflowError` is raised. The documentation warns to be careful with session data exceeding 65KB, since the default column stores Base64 encoded marshaled data.
Which serializers can activerecord-session_store use?
A class responding to `#load(value)` and `#dump(value)`, or one of the symbols `marshal`, which is the default and uses Marshal with Base64, `json`, `hybrid` which reads either and writes JSON, and `null` which leaves serialization to the database adapter.
Do I need to run anything after upgrading from version 1.x?
Yes. Sessions created by version 1.x are affected by CVE-2019-25025, a timing attack against stored session ids, so after upgrading to 2.0.0 you should run the `db:sessions:upgrade` rake task, which is idempotent and leaves user session data intact.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/rails-activerecord-session-store)Was this useful?