Inside Ruby Debuggers: TracePoint, Instruction Sequence, and CRuby API
Hello, Ruby developers! Debugging is a key part of software development, but most developers use debuggers without knowing how they actually work. The RubyMine team has spent years developing debugging tools for Ruby, and we want to share some of the insights we’ve gained along the way. In this post, we’ll explore the main technologies […]

Hello, Ruby developers!
Debugging is a key part of software development, but most developers use debuggers without knowing how they actually work. The RubyMine team has spent years developing debugging tools for Ruby, and we want to share some of the insights we’ve gained along the way.
In this post, we’ll explore the main technologies behind Ruby debuggers — TracePoint, Instruction Sequence, and Ruby’s C-level debugging APIs.
We’ll begin with TracePoint and see how it lets debuggers pause code at key events. Then we’ll build a minimal debugger to see it in action. Next, we’ll look at Instruction Sequences to understand what Ruby’s bytecode looks like and how it works with TracePoint. Finally, we’ll briefly cover Ruby’s C-level APIs and the extra power they offer.
This blog post is the second in a series based on the Demystifying Debuggers talk by Dmitry Pogrebnoy, RubyMine Team Leader, presented at EuRuKo 2024 and RubyKaigi 2025. If you haven’t read the first post yet, it’s a good idea to start there. Prefer video? You can also watch the original talk here.
Ready? Let’s start!
The core technologies behind any Ruby debugger
Before diving into the debugger internals, it’s essential to understand the two core technologies that make Ruby debugging possible: TracePoint and Instruction Sequence. Regardless of which debugger you use, they all rely on these fundamental features built into Ruby itself. In the following sections, we’ll explore how each of them works and why they’re so important.
TracePoint: Hooking into Code Execution
Let’s begin with TracePoint, a powerful instrumentation technology introduced in Ruby 2.0 back in 2013. It works by intercepting specific runtime events such as method calls, line executions, or exception raises and executing custom code when these events occur. TracePoint works in almost any Ruby context, and it works well with Thread and Fiber. However, it currently has limited support for Ractor.
Let’s take a look at the example and see how TracePoint works.
def say_hello puts "Hello Ruby developers!" end TracePoint.new(:call) do |tp| puts "Calling method '#{tp.method_id}'" end.enable say_hello # => Calling method 'say_hello' # => Hello Ruby developers!
In this example, we have a simple say_hello
method containing a puts
statement, along with a TracePoint
that watches events of the call
type. Inside the TracePoint
block, we print the name of the method being called using method_id
. Looking at the output in the comments, we can see that our TracePoint
is triggered when entering the say_hello
method, and only after that do we see the actual message printed by the method itself.
This example demonstrates how TracePoint lets you intercept normal code execution at specific points where special events occur, allowing you to execute your own custom code. Whenever your debugger stops on a breakpoint, TracePoint is in charge. This technology is valuable for more than just debugging. It is also used in performance monitoring, logging, and other scenarios where gaining runtime insights or influencing program behavior is necessary.
Building the simplest Ruby debugger with TracePoint
With just TracePoint technology, you can build what might be the simplest possible Ruby debugger you’ll ever see.
def say_hello puts "Hello Ruby developers!" end TracePoint.new(:call) do |tp| puts "Call method '#{tp.method_id}'" while (input = gets.chomp) != "cont" puts eval(input) end end.enable say_hello
This is almost the same code as in the TracePoint example, but this time the TracePoint
code body is slightly changed.
Let’s examine what’s happening here. The TracePoint
block accepts user input via gets.chomp
, evaluates it in the current context using the eval
method, and prints the result with puts
. That’s really all there is to it — a straightforward and effective debugging mechanism in just a few lines of code.
This enables one of the core features of a debugger — the ability to introspect the current program context on each method invocation and modify the state if needed. You can, for example, define a new Ruby constant, create a class on the fly, or change the value of a variable during execution. Simple and powerful, right? Try to run it by yourself!
Clearly, this isn’t a complete debugger — it lacks exception handling and many other essential features. But when we strip away everything else and look at the bare bones, this is the fundamental mechanism that all Ruby debuggers are built upon.
This simple example demonstrates how TracePoint serves as the foundation for Ruby debuggers. Without TracePoint technology, it would be impossible to build a modern Ruby debugger.
Instruction Sequence: Ruby’s bytecode revealed
Another crucial technology for Ruby debuggers is Instruction Sequence.
Instruction Sequence, or iseq
for short, represents the compiled bytecode that the Ruby Virtual Machine executes. Think of it as Ruby’s “assembly language” — a low-level representation of your Ruby code after compilation into bytecode. Since it’s closely tied to the Ruby VM internals, the same Ruby code can produce a different iseq
in different Ruby versions, not just in terms of instructions but even in their overall structure and relationships between different instruction sequences.
Instruction Sequence provides direct access to the low-level representation of Ruby code. Debuggers can leverage this feature by toggling certain internal flags or even modifying instructions in iseq
, effectively altering how the program runs at runtime without changing the original source code.
For example, a debugger might enable trace events on a specific instruction that doesn’t have one by default, causing the Ruby VM to pause when that point is reached. This is how breakpoints in specific language constructions and stepping through chains of calls work. The ability to instrument bytecode directly is essential for building debuggers that operate transparently, without requiring the developer to insert debugging statements or modify their code in any way.
Let’s take a look at how to get an Instruction Sequence in Ruby code.
def say_hello puts "Hello Ruby developers