Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Use SQLite3 as Your RateLimiter in Ruby

Sometimes, your project is small, scalability is a problem for later and you can't afford to deploy Redis separately either. In that case, you can always rely on SQLite3 to gatekeep users that might be making too many requests.

# frozen_string_literal: true

require 'dotenv/load'
require 'sqlite3'

# The @limit and @period instance variables specify
# the maximum number of requests and the time period,
# respectively.
class RateLimiter
  def initialize(app, limit:, period:, db_path: 'data/rate-limiter.db')
    @app = app
    @limit = limit
    @period = period

    @db = SQLite3::Database.new(db_path)
    create_table
  end

  def call(env)
    req = Rack::Request.new(env)
    ip = req.ip

    current_time = Time.now.to_i
    window_start = current_time - @period

    @db.execute('DELETE FROM rate_limits WHERE timestamp < ?', window_start)

    count = @db.get_first_value('SELECT COUNT(*) FROM rate_limits WHERE ip = ?', ip)

    return [429, { 'content-type' => 'text/plain' }, ['rate limit exceeded']] if count > @limit

    @db.execute('INSERT INTO rate_limits (ip, timestamp) VALUES (?, ?)', [ip, current_time])

    @app.call(env)
  end

  private

  def create_table
    @db.execute <<-SQL
      CREATE TABLE IF NOT EXISTS rate_limits (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        ip TEXT NOT NULL,
        timestamp INTEGER NOT NULL
      );
    SQL
  end
end