asynchronous logging
thin server
sinatra framework
rack middleware
web development

How do I log asynchronous thinsinatrarack requests?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

In a Thin plus Sinatra plus Rack stack, request logging can become part of the hot path if every request waits for file I/O. Asynchronous logging moves the actual write to a background worker so the Rack request thread only enqueues a log event and continues.

That idea is simple, but a good implementation still needs structure. You need one shared queue, one long-lived writer, clean shutdown, and enough metadata in each event to make the logs useful later.

Log From Middleware, Not From Every Route

Request logging is most consistent when it happens in Rack middleware. That gives you one place to capture request method, path, status code, and timing for every endpoint.

A minimal Sinatra app might look like this:

ruby
1# app.rb
2require "sinatra/base"
3
4class App < Sinatra::Base
5  get "/health" do
6    "ok"
7  end
8end

The middleware layer is where you should measure and emit request logs rather than scattering logging calls across route blocks.

Build a Queue-Based Async Logger

The usual pattern is a Queue plus one worker thread:

ruby
1# async_logger.rb
2require "json"
3require "logger"
4require "thread"
5
6class AsyncLogger
7  def initialize(path)
8    @logger = Logger.new(path)
9    @queue = Queue.new
10    @worker = Thread.new do
11      loop do
12        entry = @queue.pop
13        break if entry == :__stop__
14        @logger.info(entry.to_json)
15      end
16    end
17  end
18
19  def info(payload)
20    @queue << payload
21  end
22
23  def stop
24    @queue << :__stop__
25    @worker.join
26  end
27end

This keeps disk writes off the request path. Each request only pushes a Ruby hash into the queue.

Capture Request Details in Middleware

Now wrap the app with middleware that records request metadata and duration:

ruby
1# request_log_middleware.rb
2require "securerandom"
3
4class RequestLogMiddleware
5  def initialize(app, async_logger)
6    @app = app
7    @async_logger = async_logger
8  end
9
10  def call(env)
11    started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
12    request_id = SecureRandom.uuid
13
14    status, headers, body = @app.call(env)
15
16    duration_ms = (
17      Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
18    ) * 1000.0
19
20    @async_logger.info(
21      request_id: request_id,
22      method: env["REQUEST_METHOD"],
23      path: env["PATH_INFO"],
24      status: status,
25      duration_ms: duration_ms.round(2)
26    )
27
28    [status, headers, body]
29  rescue => e
30    @async_logger.info(
31      request_id: request_id,
32      method: env["REQUEST_METHOD"],
33      path: env["PATH_INFO"],
34      error: e.class.name,
35      message: e.message
36    )
37    raise
38  end
39end

This produces one structured event per request and keeps the logging concern out of application routes.

Wire It Up in Rack

Mount the middleware in config.ru:

ruby
1require_relative "app"
2require_relative "async_logger"
3require_relative "request_log_middleware"
4
5async_logger = AsyncLogger.new("log/requests.log")
6
7use RequestLogMiddleware, async_logger
8run App

Then start Thin as usual:

bash
bundle exec thin start -R config.ru -p 3000

At that point, requests are logged asynchronously through the shared worker.

Shutdown and Backpressure Matter

An async logger that never flushes on shutdown is incomplete. Trap process signals and stop the worker cleanly:

ruby
trap("TERM") { async_logger.stop; exit }
trap("INT")  { async_logger.stop; exit }

You should also think about queue growth. A completely unbounded queue can consume too much memory during traffic spikes or slow disk conditions. In production, teams often add:

  • queue length metrics
  • a drop policy for low-priority logs
  • synchronous fallback for critical failures

Async logging is not just a code trick. It is a small subsystem with operational behavior.

Common Pitfalls

The biggest mistake is creating a new logger thread per request. That destroys the performance benefit and adds unnecessary concurrency overhead.

Another common issue is using an unbounded queue without any monitoring. If log writes slow down under load, memory growth can become the new problem.

Developers also forget graceful shutdown. Without it, the last queued events can be lost whenever the process stops.

Finally, avoid mixing multiple output formats in one file. Structured JSON lines are much easier to consume downstream than a blend of plain text and ad hoc hashes.

Summary

  • In Sinatra and Rack, asynchronous logging is best implemented in middleware.
  • Use one shared queue and one background writer instead of logging directly from the request path.
  • Capture consistent request fields such as method, path, status, and duration.
  • Handle shutdown cleanly so queued log entries are flushed.
  • Treat queue growth and logger failure as operational concerns, not just implementation details.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.