Ruby and C

Ruby is for people; C is for machines. Most of the time you only need the first. When a measurement shows Ruby itself is the slow part, you can hand that one hot path to C and keep everything else in Ruby. This is how to do it, from least effort to most.

1. Make sure it's really Ruby

Most slow Ruby apps aren't slow because of Ruby. They wait on the database, the network or an uncached page. Profile first (stackprof, vernier, rack-mini-profiler) and confirm the time is spent running Ruby code. Then turn on YJIT, Ruby's built-in JIT compiler, which often closes the gap for free:

ruby --yjit app.rb
# or, from Ruby 3.3 on, inside the app:
RubyVM::YJIT.enable

See Caching in Rails for the usual first fixes.

2. You're already using C

Much of Ruby is C. Array#sort, String methods, Hash, and standard libraries like json, zlib, openssl and digest all run as compiled C. Often the fastest fix is to push work into one of these: call one built-in method on a whole array instead of looping in Ruby, or let the database sort instead of sorting in memory.

3. Use a battle-proven gem

For most heavy lifting, someone has already wrapped a fast, well-tested C library in a gem used by thousands of apps. Use it before writing anything yourself.

nokogiri: XML and HTML parsing on top of libxml2.
pg, mysql2, sqlite3: database drivers built on each database's own C client.
ruby-vips: image processing with libvips, fast and light on memory.
msgpack: compact, fast serialization.
bcrypt: password hashing.
puma: its HTTP parser is written in C.

Prefer gems that ship precompiled versions for common platforms, so installs don't need a compiler.

4. Call a C library directly with FFI

If the library you need has no gem, you can still call it without writing C. The ffi gem (or fiddle, in the standard library) loads a shared library and lets you declare its functions in Ruby:

require "ffi"

module LibC
  extend FFI::Library
  ffi_lib FFI::Library::LIBC
  attach_function :strlen, [:string], :size_t
end

LibC.strlen("hello") # => 5

There's no compile step and no C file to maintain. Each call has a small overhead, so FFI suits chunky calls that do real work, not millions of tiny ones.

5. Write a C extension

When you need the most speed for a tight loop, write a small C extension. Ruby's build tooling (mkmf and an extconf.rb) compiles it when the gem installs:

// ext/fast_sum/fast_sum.c
#include "ruby.h"

static VALUE fast_sum(VALUE self, VALUE ary) {
  long total = 0;
  for (long i = 0; i < RARRAY_LEN(ary); i++)
    total += NUM2LONG(rb_ary_entry(ary, i));
  return LONG2NUM(total);
}

void Init_fast_sum(void) {
  rb_define_global_function("fast_sum", fast_sum, 1);
}
# ext/fast_sum/extconf.rb
require "mkmf"
create_makefile("fast_sum")

Keep the extension tiny and the Ruby-facing API unchanged, so callers never know. Long-running C code can release Ruby's global lock so other threads keep working while it runs.

6. Know the costs

Crashes: a bug in C can take down the whole process instead of raising an exception.
Builds: extensions need a compiler and headers at install time, on every platform you deploy to.
Upkeep: fewer people can read and fix C, and Ruby's C API changes across versions.
Proof: benchmark before and after. If C doesn't win clearly, delete it and keep the Ruby.

Rust is a safer alternative for new native code; the rb-sys and magnus crates make Rust extensions about as easy as C ones.


Provided as is, for information only, with no warranty and no liability. Not legal or professional advice. Security and radio material is for authorized, lawful use only. Full disclaimer.

Visits are logged on our own server: IP address, browser and device details, and pages viewed. We use them to understand traffic and spot attacks, and never share or sell them.