Sessions in Rails: Everything You Need to Know

Sessions in Rails: Everything You Need to Know

In this post, we’ll learn about Rails sessions, including what is a session, why we need them, and why they're so important. I’ll also take you behind the scenes and show you how Rails implements sessions and where the `session` method actually comes from. Hint: it’s not in the Rails codebase.

10 min read

Most web applications need to store users' data, such as their username, email, or their preferences across multiple requests. In addition, once a user logs into your application, they need to remain logged in until they log out.

However, HTTP is a stateless protocol. In simple words, the server does not retain any information from one request to another. So how and where do we store this user-specific data that we need across multiple requests? The answer is: a session.

Sessions provide a way to store a user's information across multiple requests. Once you store some data in the session, you can access it in all future requests.

# store the username in the session
# when a user logs in
session[:user_id] = user.username

# read the username from the session
# when the user accesses the website again
username = session[:user_id]

Looks simple, right? Well, there's a lot that goes on behind the scenes to make it work. This article will show you how sessions work and how you can work with them.  

What You'll Learn

  1. What is a session?
  2. How Sessions Work
  3. Session Storage
  4. Working with Sessions
  5. How Rails Implements Sessions
  6. Resources to Learn More

Sounds fun? Let's dig in.

What is a Session?

A session can refer to one of two things:

  1. A place where you store data during one request that you can access in later requests.
  2. A time period between which the user is logged in to your application.

Once a user logs in, the application stores their information in a session (the place), and uses that information in subsequent requests to make sure the user is valid and logged in (the period).

Your application has a session for each user in which you can store small amounts of data that will be persisted between requests.

You can write some data to the session in one request:

class SessionsController < ApplicationController
  def store
    session[:user_id] = user.username
  end
end

and read it inside another request:

class SessionsController < ApplicationController
  def read
    username = session[:user_id]
  end
end

As you can see, Rails makes it extremely easy for you to work with sessions. You can simply treat it like a Ruby Hash.

How do Sessions Work?

At this point, you may be wondering: how do sessions help you maintain the state from one request to another?

The answer lies in a cookie.

Cookies
No, not these cookies!

When your browser requests a website for the first time, along with the response, the server sends a small piece of data back to the web browser. This data is called a cookie. Upon receiving this cookie, the browser stores this cookie and sends it back to the server for all future requests.

How Cookies Work
How Cookies Work
If you're curious to learn more about cookies, I recommend reading the MDN guide: Using HTTP cookies. In a nutshell, the server can set cookies using the Set-Cookie header and read cookies using the Cookie header.

The server sets a different cookie for different browsers, even if they're on the same computer. That's why cookies are typically used to tell if multiple requests came from the same browser. This lets the website remember useful information from previous requests (such as the user's unique identifier), despite HTTP being a stateless protocol.

Example: how you stay logged in on Amazon

Let's say you're logging in to Amazon for the first time. Once you log in, Amazon's back-end server will send a cookie in the response, and this cookie will be sent to the server every time you browse any pages on Amazon.

Upon receiving the cookie, Amazon's server checks if the user corresponding to that cookie is logged in, and displays a personalized web page.  

However, if you browse Amazon from a different device or even a different browser, it won't send the cookie to the server, as it doesn't have that cookie. Cookies are scoped to the browser. Hence you won't be logged in on a different device or browser.

So, how does that relate to the Sessions?

Sessions use cookies behind the scenes. Instead of you having to set and get data from the cookies, Rails handles it for you via sessions. Your Rails application has a session for each user, in which you can store data that will be persisted between multiple requests.

Rails identifies the session by a unique session ID, a 32-character string of random hex numbers. When a new user accesses the application for the first time, Rails generates this session ID and stores it inside the cookie. Once this session ID is sent to the browser, it sends it back to the server for future requests.

Session Storage

When you store some data in the session, Rails can store it in any one of the following four session stores.

  1. CookieStore: Store all data inside a cookie, in addition to the session ID
  2. CacheStore: Store data inside the Rails cache
  3. ActiveRecordStore: Store the data in the database
  4. MemCacheStore: Use a Memcached cluster of servers

All these stores can be accessed via a unified API, making it very convenient to switch from one to another.

One important thing to note here is that whichever store you use to store the session data, the actual session identifier always comes from the cookie. I feel this is a point that confuses many beginner Rails developers, and it confused me for a long time.

The server communicates the state with the browser by sending the session ID via a cookie. This cookie is used for all future requests to identify the session for that particular user, and to store additional data in any one of the session stores mentioned above.

For example, you can store that user's preferences inside the cookie (in addition to the session id), in the database, or the cache.

The best practice is to use the cookie only to store a unique ID for each session, and nothing else.

Cookies have a size limit of 4 KB. Since the browser sends cookies on each request, stuffing large data in cookies will bloat the requests. Hence you should only store the session ID in the cookie. Upon receiving the request, your application can use that identifier to fetch additional data from the database, cache, or Memcached.

Working with Sessions

I hope that by now you should have a solid understanding of what sessions are, why we need them, and how they work on a high level. In this section, we will explore how you can work with sessions in Rails on a day-to-day basis.

You can access sessions in only two places: controllers and views. Since a session (and cookies) is a concept related to HTTP, it doesn't make sense to access them inside your models, services, or anywhere else. The controllers are the entry point for all incoming HTTP requests and make the best place to access sessions.

There are two primary ways to work with session data in Rails.

  1. Use the session instance method available in the controllers. It returns a Hash-like object, and you can use it just like a regular Hash in Ruby.
  2. Use the session method on the request objects, which is useful if you need to access sessions inside your routes file.

Let's take a look at accessing sessions via the session method in the controllers. This is the most common way you'll be accessing sessions in your Rails applications.

class ProjectsController < ApplicationController
  def show
    # read data
    value = session[:key]
    name  = session[:name]
    
    # write data
    session[:key]  = value
    session[:name] = 'Akshay'
  end
end

In addition to the standard hash syntax in Ruby, you can also use the fetch method on the session. This lets you pass a default value while trying to read an item from the session. If the specified key doesn't exist in the session, Rails will return the default value you provided.

class ProjectsController < ApplicationController
  def show
    session["one"] = "1"
    session.fetch(:one) # 1
    
    session.fetch(:two, "2") # 2
    session.fetch(:two, nil) # nil
  end
end

You can also pass a block to the fetch method. This block will be executed and its result will be returned if the specified key doesn't exist. The key is yielded to the block.

class ProjectsController < ApplicationController
  def show
    session.fetch(:three) { |el| el.to_s } # "three"
  end
end

If you don't pass a default value or a block, Rails will raise an error.

class ProjectsController < ApplicationController
  def show
    session.fetch(:three) # raises KeyError
  end
end

Use the dig method if you have nested data in the session.

class ProjectsController < ApplicationController
  def show
    session["one"] = { "two" => "3" }
    session.dig("one", "two")   # 3
    session.dig(:one, "two")    # 3

    session.dig("one", :two)    # nil
  end
end

Update and Delete the Session Data

If you've already stored some data in the session and would like to update it, use the update method, providing the updated value.

class ProjectsController < Application Controller
  def new
    session["action"] = "NEW"
  end
  
  def create
    session.update(action: "CREATE")
  end
end

If you decide you don't need to keep some data in the session anymore, such as a user's ID aftere they log out, use the delete method to remove it.

class ProjectsController < Application Controller
  def new
    session.delete("action")
  end
end

Accessing Sessions Inside Routes

Sometimes, you may need to access sessions inside your routes.rb file. For this, you can call the session method on the request object available inside the route constraints.

get 'sessions', to: 'projects#sessions', constraints: ->(req) {
  req.session[:name] = 'akshay'
  true 
}, as: 'sessions'

Alternatively, you can create an instance of the ActionDispatch::Request using the env object yielded to the Rack application.

get 'sessions', to: -> (env) { 
  req = ActionDispatch::Request.new(env)
  req.session[:name] = 'Akshay'
  [200, {}, ["Success!"]] 
}

To learn more about router constraints and using a rack app inside the routes, check out this article that goes deep into routing in Rails.

Understanding the Rails Router: Why, What, and How
The router is the entry point of your Rails application. It acts as the gatekeeper for all incoming HTTP requests, inspecting and sending them to a controller action; even filtering and rejecting them if necessary. In this article, we’ll do a deep dive into the Rails Router to understand it better.

One final thing to remember: Sessions are lazily loaded. If you don't access sessions at all, they will not be loaded. Hence, you don't have to disable sessions if you aren't using them.

Digging Deeper: How Rails Implements Sessions

So far, you've learned that the session instance method provides access to the session inside the controller. Ever wondered where that method comes from?

Let's open the Rails codebase and take a peek at the ActionController::Metal class, the great-grandfather of all your controllers.

Metal Controller in Rails
The ActionController::Base is the great-grandfather of all Rails controllers, but doesn’t get much attention. In this post, we will examine this Metal class and how it works.

Here's a simplified version:

module ActionController
  class Metal
    delegate :session, to: "@_request"
  end
end

The delegate method forwards any calls to session to the @_request object, which is an instance of the ActionDispatch::Request class. Let's open it and see if we can find the session method in this class.

We can't!! There's no session method inside the ActionDispatch::Request class.

What's going on? 🤔

Let's see if it includes any modules. Yes, it does:

module ActionDispatch
  class Request
    include Rack::Request::Helpers
  end
end

Let's open the Rack codebase, go to the Rack::Request class, and there is our session method! All it does is check the value of the RACK_SESSION header variable and set it to the value returned by the default_session method.

# https://github.com/rack/rack/blob/main/lib/rack/request.rb

module Rack
  class Request
    module Helpers
      def session
        fetch_header(RACK_SESSION) do |k|
          set_header RACK_SESSION, default_session
        end
      end

      def default_session; {}; end
    end
  end
end

Let's get back to the Rails codebase. The ActionDispatch::Request class overrides the default_session method, providing its own implementation as follows:

module ActionDispatch
  class Request
    def default_session
      Session.disabled(self)
    end
  end
end

The Session.disabled method returns a new instance of the ActionDispatch::Request::Session class. Here's a simplified version of it.

module ActionDispatch
  class Request
    class Session
      def self.disabled(req)
        new(nil, req, enabled: false)
      end
    end
  end
end

And that's where the session object is created. We can go deeper, but I think we have a pretty good foundation so you can explore the Session class on your own, which I highly encourage. In a future article, we'll explore how different session stores are implemented.

Resources

  • How Rails Sessions Work: This is the original blog post from Justin Weiss, that made the sessions 'click' for me. While you're there, I recommend reading other articles from Justin.
  • Rails Guides on Sessions: Read them for more details on configuring different stores for the sessions, also tangential concepts such as the flash.
  • Laravel Sessions: To learn how Laravel uses sessions. Rails and Laravel are very similar, and I always learn new things by reading the Laravel docs.

I hope this post helped you gain a deeper understanding of sessions in Rails. Typically, you'll use sessions to implement authentication for your application. However, you can also use them to store additional data such as user preferences or UI/theme settings for the website.

If you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.

Please subscribe to my blog if you'd like to receive future articles directly in your email. If you're already a subscriber, thank you.