tj-scale
TJ Scale (tj-scale) is a Ruby gem that automatically monitors your background job queues — Delayed Job or Sidekiq — and your web traffic, and sends metrics to a remote monitoring service for auto-scaling purposes. It integrates seamlessly with Rails applications and Heroku deployments.
Features
- 🔍 Automatic Queue Monitoring: Continuously monitors Delayed Job or Sidekiq queues for pending jobs
- 🔀 Pluggable Job Backend: Flip between Delayed Job and Sidekiq with one env var (
TJ_SCALE_JOB_BACKEND); auto-detected by default - 📊 Metrics Reporting: Sends job counts, queue latency, and web queue/response times to a remote monitoring service
- 🚀 Heroku Integration: Automatically runs on the first web or worker dyno
- ⚙️ Configurable: Customizable priority/queue filters and API endpoints
- 🔔 Slack Notifications (control plane): message your team whenever web or worker dynos scale up/down, or when scaling fails
- 🔒 Error Handling: Robust error handling with comprehensive logging
- 🧪 Well Tested: Comprehensive test suite with RSpec
Control plane (Rails + PostgreSQL)
The control plane lives in its own project, tj-scale-dashboard (a sibling directory / repository to this gem): dashboard, per-app scaling rules (web queue time + worker job counts), metrics ingest API compatible with the gem, Heroku formation updates, and Slack notifications for every scale event (Settings → Slack notifications). It uses PostgreSQL — see its README.md and .env.example in ../tj-scale-dashboard/.
Installation
As a Gem
Add this line to your application's Gemfile:
gem "tj-scale", "~> 1.0"
And then execute:
$ bundle install
Or install it yourself as:
$ gem install tj-scale
From Source
- Clone the repository:
git clone https://github.com/Untechnickle/tj-scale-gem.git
cd tj-scale-gem
- Install dependencies:
bundle install
- Run the setup script:
bin/setup
End-to-end guide
Build the gem, publish or reference it, add it to your Rails app, and set configuration (env / Heroku). Follow these steps in order the first time you ship and adopt the gem.
1. Prerequisites
- Ruby 3.0+ and Bundler 2+
- A Rails 6.1+ app with one job backend:
delayed_job_active_record(and a DB-backeddelayed_jobstable), orsidekiq(and Redis)
- Heroku CLI (if you deploy to Heroku)
- A RubyGems.org account (if you publish publicly)
2. Prepare and build the gem (in this repository)
- Track files for the gem package — the gemspec uses
git ls-files, so the repo must be a git repository and files must be committed:
cd /path/to/tj-scale
git init # if needed
git add .
git commit -m "Release tj-scale"
- Install dependencies and verify quality:
bundle install
bundle exec rspec
bundle exec rake standard
Set the version in
lib/tj_scale_ruby/version.rb(for example1.0.0).Build the package:
gem build tj-scale.gemspec
This produces tj-scale-<version>.gem in the current directory.
3. Publish the gem (choose one)
RubyGems.org (public)
- Create an API key at rubygems.org/api_keys.
- Push the file you built (version must match
VERSION):
gem push tj-scale-1.0.1.gem
Private gem host
gem push tj-scale-1.0.1.gem --host https://your-gem-server.com
Configure Bundler in the app if your host needs authentication (see your provider’s docs).
Without publishing — use the repo directly
In the app’s Gemfile:
gem "tj-scale", git: "https://github.com/Untechnickle/tj-scale-gem.git", branch: "main"
# or path for local development:
# gem "tj-scale", path: "../tj-scale"
Then bundle install.
4. Add the gem to your Rails application
- Open the app’s
Gemfileand add (adjust version or source as above):
gem "tj-scale", "~> 1.0"
- Run:
bundle install
No extra
requireis needed inapplication.rb: the gem registers a Rails Railtie and loads with Rails. Ensure your app already bundles its job backend —delayed_job_active_recordorsidekiq(the gem auto-detects which one is loaded, or setTJ_SCALE_JOB_BACKENDexplicitly).Deploy or run the server as usual. The monitoring thread starts only on
web.1orworker.1, depending onTJ_SCALE_MONITOR_PROCESS(see below).
5. Configure environment variables
Configuration is entirely via environment variables (and Heroku config vars in production). Copy .env.example to .env for local use if you use dotenv-rails or similar.
| Variable | Required | Purpose |
|---|---|---|
TJ_SCALE_API_TOKEN |
Yes | Sent as LOGPLEX_DRAIN_TOKEN and Authorization: Bearer … on each ingest POST. |
TJ_SCALE_API_URL |
Yes | Full URL of the Agent API ingest path: POST /api/v1/metrics or POST /api/v1/sys-logs (same handler). Example local: http://127.0.0.1:5001/api/v1/metrics. |
TJ_SCALE_TARGET_APP |
No | Heroku app name to scale; defaults to HEROKU_APP_NAME if unset. |
TJ_SCALE_TARGET_PROCESS |
No | web or worker to scale; defaults to TJ_SCALE_MONITOR_PROCESS. |
TJ_SCALE_MONITOR_PROCESS |
No | web or worker — only that process’s .1 dyno runs the reporter (default worker). |
TJ_SCALE_INTERVAL_SECONDS |
No | Seconds between posts (default 20). |
TJ_SCALE_JOB_BACKEND |
No | Queue backend for the worker reporter: delayed_job, sidekiq, or auto (default). auto uses Sidekiq when the sidekiq gem is loaded, otherwise Delayed Job. |
TJ_SCALE_SIDEKIQ_QUEUES |
No | Sidekiq backend only: comma-separated queue names to count (empty = all queues). |
TJ_MIN_PRIORITY / TJ_MAX_PRIORITY |
No | Limit which job priorities are counted (0 = no filter; worker reporter, Delayed Job backend only). |
TJ_SCALE_QUEUE_TIME_MS |
No | When TJ_SCALE_MONITOR_PROCESS=web, supplies queue_time_ms if no middleware snapshot exists. |
TJ_SCALE_ENABLE_QUEUE_TIME_MIDDLEWARE |
No | Set to 1, true, yes, or on to enable Rack middleware that records queue_time_ms from X-Request-Start (Heroku). |
Payload by reporter: web — queue_time_ms (when known), job_count (requests since last tick), optional response_time_ms. worker — job_count, queue_time_s (oldest waiting job age / queue latency, or 0), and job_backend (delayed_job or sidekiq). Both send heroku_app and target_process. Scaling limits (min/max dynos) are configured in the control plane's dashboard settings, not by this gem. Autoscaling runs on the control plane after ingest; this gem does not call Heroku.
Heroku sets DYNO (for example web.1, worker.1) and typically HEROKU_APP_NAME; do not set DYNO manually in production.
Example: my-app-web (web) and my-app-workers (workers)
Use the same token on both apps if one backend handles scaling.
# my-app-web — reporter on web.1, scale web dynos
heroku config:set TJ_SCALE_API_TOKEN=your_token \
TJ_SCALE_API_URL=https://your-control-plane.example.com/api/v1/metrics \
TJ_SCALE_TARGET_APP=my-app-web \
TJ_SCALE_TARGET_PROCESS=web \
TJ_SCALE_MONITOR_PROCESS=web \
-a my-app-web
# my-app-workers — reporter on worker.1, scale worker dynos
heroku config:set TJ_SCALE_API_TOKEN=your_token \
TJ_SCALE_API_URL=https://your-control-plane.example.com/api/v1/metrics \
TJ_SCALE_TARGET_APP=my-app-workers \
TJ_SCALE_TARGET_PROCESS=worker \
TJ_SCALE_MONITOR_PROCESS=worker \
-a my-app-workers
On the web app, enable TJ_SCALE_ENABLE_QUEUE_TIME_MIDDLEWARE=1 (and restart) so queue_time_ms is populated from Heroku’s router; the web dyno does not need a shared Delayed Job DB for that path.
6. Wire your monitoring service (Agent API)
The TJ Scale Agent API accepts the same JSON on either route:
POST /api/v1/metricsPOST /api/v1/sys-logs
Set TJ_SCALE_API_URL to the full URL of one of them (for example http://127.0.0.1:5001/api/v1/metrics in development). Authenticate with the ingest token using LOGPLEX_DRAIN_TOKEN or Authorization: Bearer (same secret as TJ_SCALE_API_TOKEN); the gem sends both headers.
The gem POSTs JSON with timestamp, heroku_app, target_process, plus web metrics (queue_time_ms, job_count, optional response_time_ms) or worker metrics (job_count, queue_time_s). Your control plane ingests samples and applies scaling rules (thresholds, cooldowns) when calling the Heroku Platform API.
7. Verify
- At least one web dyno on
my-app-weband one worker dyno onmy-app-workerssoweb.1andworker.1exist. - Check Rails logs for
TjScaleRubylines (debug/warn/error). - Confirm your API receives periodic requests with the expected payload.
Optional: manual calls in Ruby
TjScaleRuby::JobMonitor.pending_job_count
TjScaleRuby::JobMonitor.oldest_pending_queue_seconds
TjScaleRuby.record_web_queue_time_ms!(42) # optional; middleware does this on each request
TjScaleRuby::JobMonitor.send_log
Configuration
Environment Variables
Create a .env file in your project root (or set these in your Heroku config vars):
# Required: API token and ingest URL for your control plane
TJ_SCALE_API_TOKEN=your_api_token_here
TJ_SCALE_API_URL=http://127.0.0.1:5001/api/v1/metrics
# --- Scaling metadata (sent to your monitoring API; defaults shown) ---
# Heroku app name to scale (defaults to HEROKU_APP_NAME when unset).
TJ_SCALE_TARGET_APP=my-heroku-app
# Process type to scale on that app: "web" or "worker" (defaults to TJ_SCALE_MONITOR_PROCESS).
TJ_SCALE_TARGET_PROCESS=web
# Which process type runs the reporter loop: only the first dyno of this type (web.1 or worker.1).
# Use "web" on the web app, "worker" on the worker app.
TJ_SCALE_MONITOR_PROCESS=worker
# Seconds between metric posts (default 20).
TJ_SCALE_INTERVAL_SECONDS=20
# Optional: Minimum priority filter for jobs (0 = no filter)
# Only count jobs with priority >= this value
TJ_MIN_PRIORITY=0
# Optional: Maximum priority filter for jobs (0 = no filter)
# Only count jobs with priority <= this value
TJ_MAX_PRIORITY=0
Heroku sets HEROKU_APP_NAME for the current app. If you omit TJ_SCALE_TARGET_APP, the payload uses that value so each app reports scaling targets for itself.
Heroku Configuration
Set the environment variables on Heroku:
heroku config:set TJ_SCALE_API_TOKEN=your_api_token_here
heroku config:set TJ_SCALE_API_URL=https://your-control-plane.example.com/api/v1/metrics
The DYNO environment variable is automatically set by Heroku and is used to determine which dyno should run the monitoring loop (only web.1 or worker.1, depending on TJ_SCALE_MONITOR_PROCESS).
Example: web on my-app-web, workers on my-app-workers
Use two Heroku apps (or one app with both process types—same idea). Configure each app’s config vars so the monitoring service receives heroku_app, target_process, and job_count on each POST. Scaling bounds (min/max dynos) are set in the control plane's dashboard.
App my-app-web (web dynos) — run the reporter on the first web dyno and scale web dynos:
heroku config:set TJ_SCALE_API_TOKEN=your_token -a my-app-web
heroku config:set TJ_SCALE_MONITOR_PROCESS=web -a my-app-web
heroku config:set TJ_SCALE_TARGET_APP=my-app-web -a my-app-web
heroku config:set TJ_SCALE_TARGET_PROCESS=web -a my-app-web
heroku config:set TJ_SCALE_API_URL=https://your-control-plane.example.com/api/v1/metrics -a my-app-web
heroku config:set TJ_SCALE_ENABLE_QUEUE_TIME_MIDDLEWARE=1 -a my-app-web
App my-app-workers (worker dynos) — run the reporter on worker.1 and scale worker dynos:
heroku config:set TJ_SCALE_API_TOKEN=your_token -a my-app-workers
heroku config:set TJ_SCALE_MONITOR_PROCESS=worker -a my-app-workers
heroku config:set TJ_SCALE_TARGET_APP=my-app-workers -a my-app-workers
heroku config:set TJ_SCALE_TARGET_PROCESS=worker -a my-app-workers
heroku config:set TJ_SCALE_API_URL=https://your-control-plane.example.com/api/v1/metrics -a my-app-workers
Ensure both apps have the web / worker process types in the Procfile as needed, deploy, and scale at least one dyno of each monitored type so web.1 / worker.1 exist.
Monitoring API: Your receiver at TJ_SCALE_API_URL should read the JSON body and enforce the min/max dyno bounds from its own dashboard settings when calling the Heroku Platform API (or your own scaler). Fields sent include job_count, timestamp, and when set, heroku_app and target_process.
Usage
Automatic Monitoring
Once installed and configured, the gem starts monitoring on the first dyno of the configured process type (web.1 or worker.1, see TJ_SCALE_MONITOR_PROCESS). The loop runs every TJ_SCALE_INTERVAL_SECONDS (default 20) and POSTs metrics to the configured API.
Manual Usage
You can also use the gem's methods manually in your code:
# Get the count of pending jobs
count = TjScaleRuby::JobMonitor.pending_job_count
puts "Pending jobs: #{count}"
# Send current job count to the monitoring service
response = TjScaleRuby::JobMonitor.send_log
if response
puts "Log sent successfully"
end
Priority Filtering
You can filter jobs by priority using environment variables:
# Only count jobs with priority between 5 and 10
ENV['TJ_MIN_PRIORITY'] = '5'
ENV['TJ_MAX_PRIORITY'] = '10'
How It Works
- Initialization: When your Rails application starts, the
Railtiechecks if it's running on the first monitored dyno (web.1orworker.1, perTJ_SCALE_MONITOR_PROCESS) - Background Thread: If conditions are met, a background thread is started
- Monitoring Loop: Every N seconds (
TJ_SCALE_INTERVAL_SECONDS), the thread POSTs a payload: on worker reporters it counts waiting jobs on the active backend; on web reporters it sends router queue time (middleware / env) plus request volume and average response time for the interval. - Job counting (worker reporters, Delayed Job backend):
pending_job_countfilters jobs based on:- Jobs past their
run_attime (with 5 second buffer) - Jobs that haven't failed (
failed_at IS NULL) - Jobs that aren't locked (
locked_at IS NULL) - Optional priority range filters
- Jobs past their
- Job counting (worker reporters, Sidekiq backend):
pending_job_countsums enqueued jobs across queues (all queues, or only those inTJ_SCALE_SIDEKIQ_QUEUES), andqueue_time_sis the latency of the slowest monitored queue.
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
Running Tests
# Run all tests
bundle exec rspec
# Run tests with documentation format
bundle exec rspec --format documentation
# Run a specific test file
bundle exec rspec spec/lib/tj_scale_ruby/models/tj_scale_ruby_spec.rb
Code Quality
The project uses Standard for code formatting:
# Check code style
bundle exec rake standard
# Auto-fix code style issues
bundle exec rake standard:fix
Building and Publishing the Gem
For a full checklist (git, tests, version, integrate in the app, Heroku), see End-to-end guide above.
Building the Gem
- Update the version in
lib/tj_scale_ruby/version.rb - Ensure files are tracked by git (the gemspec uses
git ls-files). - Build the gem:
gem build tj-scale.gemspec
This will create a .gem file in the current directory.
Publishing to RubyGems
- Make sure you have a RubyGems account
- Get your API key from https://rubygems.org/api_keys
- Configure your credentials:
gem push --key your_api_key tj-scale-1.0.1.gem
Or use the credentials file:
# Create ~/.gem/credentials
# Add your API key:
# ---
# :rubygems_api_key: your_api_key_here
gem push tj-scale-1.0.1.gem
Publishing to a Private Gem Server
If you're using a private gem server:
gem push tj-scale-1.0.1.gem --host https://your-gem-server.com
Deployment
Heroku Deployment
- Add the gem to your
Gemfile:
gem "tj-scale", "~> 1.0"
- Set the required environment variables:
heroku config:set TJ_SCALE_API_TOKEN=your_token
- Deploy your application:
git push heroku main
The gem will automatically start monitoring on the first worker dyno.
Docker Deployment
- Add the gem to your
Gemfile - Set environment variables in your Docker configuration
- The gem will automatically start when the Rails application initializes
Other Platforms
The gem works with any Rails application. Just ensure:
- Delayed Job or Sidekiq is configured
- Environment variables are set
- The application has worker dynos (for automatic monitoring)
Requirements
- Ruby >= 3.0.0
- Rails >= 6.1
- One job backend (worker reporters only; bring your own gem):
- delayed_job_active_record >= 4.1 and PostgreSQL, or
- sidekiq and Redis
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/Untechnickle/tj-scale-gem.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Add tests for your changes
- Ensure all tests pass (
bundle exec rspec) - Ensure code style is correct (
bundle exec rake standard) - Commit your changes (
git commit -am 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
The gem is available as open source under the terms of the MIT License.
Changelog
See CHANGELOG.md for a list of changes and version history.
Support
For issues, questions, or contributions, please open an issue on the GitHub repository.
Authors
- Tanuj - Initial work - tanuj@untechnickle.com
Acknowledgments
- Built for Untechnickle
- Works with Delayed Job and Sidekiq for background job processing