Fix a Ruby pg Segfault on macOS When Running Rails Tests in Parallel

Parallel Rails tests crashed with a segfault in the pg gem on macOS. This post shows how to find the cause in crash logs and how to fix it with one line in database.yml.

I use minitest to run parallel tests with bin/rails test on GemChat (Rails 8.1, Ruby 4.0.7, pg 1.6.3, PostgreSQL in Docker).

Running the Rails test suite in parallel should make it faster.

Instead, on my Mac, it made Ruby segfault:

❯ bin/rails test
≈ tailwindcss v4.3.3

Done in 134ms
Running 838 tests in parallel using 10 processes
Run options: --seed 8608

# Running:

/Users/zoras/.local/share/mise/installs/ruby/4.0.7/lib/ruby/gems/4.0.0/gems/pg-1.6.3-arm64-darwin/lib/pg/connection.rb:944: [BUG] Segmentation fault at 0x0000000124e34971
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin25]

The parent process never stopped. It used 0% CPU and ignored everything except Ctrl+C. Serial runs with PARALLEL_WORKERS=1 always passed. For months, the workaround was “do not use parallel mode on macOS.”

This post shows the real cause and the one-line fix.


Reproduce the crash without blocking your terminal

A crash that hangs the parent process blocks your shell. Run the suite in the background and read the log file:

nohup bin/rails test > /tmp/parallel_test.log 2>&1 < /dev/null & disown
# ... wait, then read the log file ...
tail -c 2500 /tmp/parallel_test.log
ps aux | grep "[b]in/rails test"   # the parent process is still there, at 0.0% CPU

Note: Rails uses parallel mode only above 50 tests. Single-file runs show “Running 27 tests in a single process” and never reproduce a parallel-only crash. Use the full suite.


Step 1: Read the Ruby crash log

The log file holds one [BUG] report for each crashed worker. The Ruby frames were identical in each report:

c:0060 CFUNC  :connect_start
c:0059 METHOD .../pg-1.6.3-arm64-darwin/lib/pg/connection.rb:944
c:0058 METHOD .../pg-1.6.3-arm64-darwin/lib/pg/connection.rb:871
c:0057 METHOD .../pg-1.6.3-arm64-darwin/lib/pg.rb:88  (PG.connect)
.../activerecord-8.1.3.1/.../postgresql_adapter.rb  (new_client → connect)
.../active_record/test_databases.rb:22  (create_and_load_schema)

A forked worker crashed on its first database connection, during parallel database setup. It did not run a query. It did not run a migration. It crashed on connect.

Each crashed worker had exactly 3 Ruby threads: the main thread, a drb.rb thread (Rails uses DRb to control parallel workers), and an AR Pool Reaper thread. This is standard Rails parallel machinery. No unknown code started threads.


Step 2: Read the macOS crash log

The Ruby report points at connect_start, but that function is a thin wrapper. The real data is in ~/Library/Logs/DiagnosticReports/ruby-*.ips: EXC_BAD_ACCESS / KERN_INVALID_ADDRESS on the main thread. The C stack shows the fault (deepest frame last):

PQconnectStart → pqConnectDBStart → PQconnectPoll
  → init_allowed_encryption_methods → select_next_encryption_method
  → pg_GSS_have_cred_cache → gss_acquire_cred → ... krb5 ...
  → xpc_connection_resume → ... → SIGSEGV

The crash is not in Postgres communication. It is in the Kerberos encryption check inside libpq, in macOS system code, before one byte goes to the database.


The cause: gssencmode=prefer + fork() + macOS XPC

The three facts combine as follows:

  1. libpq uses gssencmode=prefer as the default value, so each new connection first looks for Kerberos credentials to try GSS encryption.
  2. On macOS, that check uses the system GSS/Kerberos code, which talks to a credentials service through XPC.
  3. XPC connections do not survive fork(). The parallel worker receives a dead XPC handle from the parent process and crashes when libpq uses it.

Each fact explains one symptom:

Symptom Cause
Crash occurs only in parallel mode Serial runs never call fork(); the parent XPC connection stays alive
Crash occurs only on macOS Linux reads credentials from files, which are safe across fork()
Crash never occurs in CI (Linux) Same cause - the pipeline never saw this failure
Parent process hangs at 0% CPU Dead workers never answer over DRb; the coordinator waits without end
Docker Postgres is not related The server has no Kerberos, so the check was pure cost even when it did not crash

To prove the cause before the code change, set the environment variable that libpq reads directly (no code change necessary):

PGGSSENCMODE=disable bin/rails test

The fix: one conditional block

No code in this loop uses Kerberos, so disable the check where connections are made, in config/database.yml. Scope it to macOS only, per the upstream recommendation - Linux keeps the libpq defaults:

default: &default
  # ... host, port, user, password, database, pool ...
  # libpq's default gssencmode=prefer probes the macOS Kerberos credential
  # cache over XPC on every connect, which segfaults forked processes
  # (parallel tests) holding a dead XPC handle. No PostgreSQL in this stack
  # uses GSS/Kerberos. Scoped to macOS only; Linux keeps libpq defaults.
  # https://github.com/ged/ruby-pg/issues/538#issuecomment-1610120680
  <% if RUBY_PLATFORM =~ /darwin/ %>
  gssencmode: disable
  <% end %>

Two notes. First, ActiveRecord sends valid keys directly to PG::Connection.connect - and gssencmode is valid because it is in PG::Connection.conndefaults_hash. No adapter patch is necessary. I verified the rendered config on both platforms: macOS yields gssencmode="disable", Linux yields nothing. Second, this application has four test databases (primary, cache, cable, queue), and parallel mode makes suffixed copies of each one per worker. The single conditional in the shared default: block covers all of them. Production runs on Linux, so production connection behavior does not change.

Result with the fix, for the full suite on 10 parallel workers:

838 runs, 3074 assertions, 0 failures, 0 errors, 0 skips
Finished in 6.036508s

No segfaults, clean exit, and the coverage report shows all ten workers merged under Unit Tests (subprocess: 1–10).


Other workarounds

Max’s notes on this same segfault (2023, pg 1.5.3) list the workarounds. All of them disable GSS in some location:

Workaround Reason not selected
PGGSSENCMODE=disable environment variable It works, but it is implicit - the next person to debug connections will not know why tests need it
gssencmode: disable in database.yml, scoped to macOS Selected. It is committed, minimal, and self-documenting with a comment
Postgres built without --with-gssapi It fixes one computer, not the team; Homebrew ships GSSAPI enabled
Preload-then-fork (preload_app!) This option is Puma-specific; it does not apply to Minitest workers

Refer also to the upstream discussion in ged/ruby-pg issues #311 and #538 .


Takeaway

When a native gem crashes only after fork(), read the C frames, not only the Ruby frames. The Ruby backtrace said “connect failed,” which sends you to database configuration, suffixed database names, and Docker networking. The C backtrace said “Kerberos credential check through XPC” - a subsystem your application never requested, doing work you never wanted, in a process state (after fork) in which it cannot work.

Saroj Maharjan

Ruby on Rails Consultant building reliable Rails systems and practical AI products.

Berlin, Germany https://sarojmaharjan.com