chat: sidekiq, the GVL & your queue · feeding → background-jobs.md REV 3
tomo · 09:12
Morning. I ran a 4-probe assessment on background jobs — two blanks: GVL mechanics and the pool diagnosis. I seeded a page from it, calibrated to that radar.
radar: troubleshooting 0.15
you · 09:19
honestly I still don't get why 25 sidekiq threads run "at once" if MRI has a lock
tomo · 09:19
They don't — not on CPU. One thread runs Ruby at a time; the other 24 overlap on I/O waits. I rewrote §2 around the model you're missing, with a kitchen analogy (your strongest axis).
you · 09:26
that reminds me — at InfoTech we had multitenant jobs dying and I re-enqueued them by hand for weeks
tomo · 09:27
That memory is gold. You lived at-least-once delivery, the retry schedule and ambient tenant loss — without their names. §3 now decodes your incident, and a retrieval check on troubleshooting is scheduled.
continue the thread — scripted demo
Vol. II — Rails at depth · entry 07

Background Jobs in Ruby on Rails

living article · rewritten by this chat, live · voice: your tutor · reader model: angel@2026-07-18

frontmatter · machine-readableupdated 09:41
titleBackground Jobs in Ruby on Rails
statusliving · rev 3 · 3 sections rewritten today
created2026-07-18
mastery_level0.27 / 1.0
repetition_dueoverdue  troubleshooting · today
sourceslive assessment (4 probes) · memory: InfoTech incident · this chat ←
tagsrails · backend · concurrency · architecture
feedsdecision → [[mysecondbrain.work]] queue choice
Formal definition — 0.30 Feynman explanation — 0.25 Analogical mapping — 0.35 (inferred) Implementation — 0.30 Troubleshooting — 0.15 definition 0.30 feynman 0.25 analogy 0.35* implement. 0.30 tshoot 0.15
you · live senior interview bar (0.80)
cognitive_radar from this morning's assessment — updates as you chat.
*analogy inferred from your writing, untested.
tomo · why this page reads the way it does

I assessed you this morning: four probes, two blanks. You can name the GVL but declined to explain it. You have shipped deliver_later but never turned a concurrency knob. And at InfoTech you watched multitenant jobs die and re-ran them by hand — experience without a model, which is why it never compounded. So this page spends its depth on mechanism and diagnosis, not API tours, and it leans on analogies because that is your strongest axis. When your troubleshooting score crosses 0.6, I will rewrite it terser.

§0 · prerequisites2 missing2 shaky

The low-level floor this page stands on

  • Processes vs threadsmissing A process is a running program with its own private memory; if it dies, nothing else notices. A thread lives inside a process and shares its memory with every other thread there — cheaper to create, riskier to share. Puma and Sidekiq are thread-based; Unicorn and Resque are process-based. Every trade-off below reduces to this split.
  • Blocking I/Omissing When your code queries Postgres, the CPU doesn't "work slower" — that thread does nothing at all until bytes come back. A 40 ms query is an eternity of idle CPU. Concurrency exists to spend those waits running someone else's code.
  • The GIL (today: GVL)shaky MRI's Global VM Lock lets exactly one thread execute Ruby at any instant. It doesn't forbid threads; it schedules them. You could name it this morning but not explain it — §2 exists for you.
  • Connection poolsuntested A pool is a box of N reusable connections; a thread checks one out, uses it, puts it back. When more threads want connections than the box holds, they queue — and after 5 seconds ActiveRecord raises the exact error you couldn't diagnose this morning. §4.
contents · per-section state
§1stable

What a job actually is

Strip the mystique first. A background job is three ordinary things: a piece of JSON describing work — class name, arguments, queue —; a data structure holding it — a Redis list, or a Postgres table —; and a separate OS process that loops forever: pop, deserialize, call perform, repeat.

UserMailer.welcome(user).deliver_later does not send an email. It serializes intent and pushes it onto a list. Your web request answers fast because it did almost nothing. Everything else in this page is what happens inside that third thing — the process you never see.

interview signal

Saying "a queue is just a list that another process polls" — calmly, before naming any gem — is a seniority tell. Most candidates hand-wave the transport and jump to Sidekiq trivia.

§2rewritten today

The lie in concurrency: 25

This morning I asked why 25 threads make progress under a lock, and you chose "not clear to me." Fair. Here is the model, once, properly.

Under MRI, only one thread executes Ruby at any instant — that's the GVL. But the moment a thread performs I/O — a Postgres query, an HTTP call, a Redis command — it releases the lock and goes to sleep until the kernel has bytes for it. Another thread grabs the lock and runs. Sidekiq jobs are overwhelmingly I/O-bound, so 25 threads means 25 overlapping waits and roughly one thread executing Ruby at a time. That is concurrency during I/O waits — not CPU parallelism.

Your kitchen version: one chef (the interpreter), twenty-five pans (threads). The chef never fries two eggs simultaneously — but while pan 7 sizzles unattended (I/O), the chef stirs pan 12. A kitchen that is all chopping and no sizzling — CPU-bound work like image processing — gains nothing from more pans. It needs more chefs: processes, each with its own GVL. That is why Sidekiq lets you run several processes, and why Resque chose forking outright.

interview signal

The sentence they are listening for: "The GVL gives you concurrency during I/O waits, not CPU parallelism — so I scale threads for I/O-bound jobs and processes for CPU-bound ones."

§3from memory

Your InfoTech incident, decoded

At InfoTech you ran a multitenant app where jobs kept dying, and you re-enqueued them by hand. You lived through three concepts that day without their names. Let's attach them.

your conceptual evolution · reconstructed 2026-07-18
Where you started
"Jobs sometimes die; when they do, someone re-runs them." Retry was a chore a human performed, not a mechanism the system owned.
The catalyst
Multitenant jobs failing repeatedly — and manual re-enqueues sometimes working, sometimes double-executing, never explaining themselves.
The shift
A job is a message to a stranger, not a method call to yourself. It will run later, on another thread, maybe another day — and it must carry everything it needs inside its arguments.

Name one: at-least-once delivery. Sidekiq promises your job runs at least once — never exactly once. Crashes mid-run mean re-runs; re-runs mean duplicates must be survivable. Name two: the retry schedule. A failing job retries ~25 times over ~20 days with exponential backoff, then lands in the Dead set. Your manual re-enqueues were you re-implementing that machinery — without its bookkeeping. Name three: ambient context loss. Anything that lived in thread-local state — the current tenant, above all — is gone when the job wakes up elsewhere. Multitenant jobs that "die mysteriously" are usually jobs that woke up stateless.

The cure has a name too: idempotency. Design perform so that running twice equals running once — upserts instead of creates, guard clauses on state (return if invoice.sent?), uniqueness keys for the truly critical. Once a job is idempotent, retries stop being scary and become what they were meant to be: free reliability.

friction index 0.50 · 2 of 4 probes blank · high-yield page
pattern detected: experience without a model. You operated the fix (manual retries) without the delivery-semantics frame, so nothing compounded. Next incident, before pasting a stack trace, write three lines: failure hypothesis + environment constraints + one specific question. That prompt shape is how seniors debug — and how this wiki learns fastest from you.
retrieval duetroubleshooting · 0.15 · expired 2026-07-18

Before you read §4 — you raise Sidekiq's concurrency from 10 to 30 and ActiveRecord::ConnectionTimeoutError starts flooding the logs. Say your first hypothesis out loud. Then check.

reveal the answer

The ActiveRecord connection pool is still smaller than your thread count. Thirty threads compete for (say) ten connections; the losers wait, and after the 5-second checkout timeout they raise exactly that error. Fix: pool ≥ concurrency in the Sidekiq process — then verify Postgres max_connections can absorb pool × processes.

You blanked on this at 09:14 today. If you just got it right, §3 is doing its job — next review lands 2026-07-21.

§4new today

Pools: the diagnostic you missed

Every thread that touches ActiveRecord checks out a database connection and keeps it for the duration of its work. The pool size comes from database.yml — historically pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>. Raise Sidekiq to 30 threads without touching that number and you have built a bottleneck on purpose: 30 workers, 5 connections, 25 threads queuing at the box. Five seconds later, the exception.

The arithmetic that matters end-to-end: total Postgres connections ≈ (pool × Sidekiq processes) + (pool × web processes), and that sum must clear the server's max_connections — which is why teams put PgBouncer in front long before they "need microservices." Redis has a pool too, but Sidekiq sizes its own to match concurrency; the pool you personally own is ActiveRecord's.

The instinct you lacked this morning was not knowledge of an obscure flag. It was a habit: read the error class and ask who hands out the resource it names. ConnectionTimeoutError names a connection pool. Start there; compare concurrency vs pool; check the server's headroom; only then blame Redis, the GVL, or the infra. That four-step ritual is the troubleshooting axis on your radar.

interview signal

"What breaks when you raise Sidekiq concurrency?" is a classic senior screen. The expected shape: DB pool first, then max_connections, then memory-per-thread, then GVL saturation for CPU-bound jobs.

§5decision pending

Sidekiq vs Solid Queue, 2026

You know Solid Queue exists — Rails 8 made it the default — but not its internals. Here is the decision, compressed. Solid Queue keeps jobs in Postgres; workers claim them with SELECT … FOR UPDATE SKIP LOCKED, a row-level "take what nobody holds, skip what somebody does" that avoids both double-claims and lock convoys. No Redis, no extra process to babysit, and you can enqueue inside the same transaction as your domain data — the job cannot exist unless the order does. With Redis-backed Sidekiq, that atomicity is your discipline (after_commit), not the system's.

dimensionSidekiqSolid Queue
store / transportRedis list, BRPOP push semanticsPostgres table, polling + SKIP LOCKED
extra infraRedis (one more thing on call)none you don't already run
latencysub-millisecond pickuppolling interval (~100 ms+) — fine for almost everything
throughput ceilingvery high (memory-speed)thousands of jobs/min before DB churn matters
transactional enqueuemanual discipline (after_commit)native — same transaction as your data
ecosystembatches, unique jobs, cron, Pro supportrecurring jobs built in; younger ecosystem
verdict for [[mysecondbrain.work]]

Your Graphiti ingestion is bursty and low-volume: dozens-to-hundreds of documents per run, each dominated by minutes-long LLM and embedding calls. Latency is irrelevant; operational simplicity on your own machine (Colima, one Docker daemon already) is everything — and you already run Postgres.

Start with Solid Queue. Adopt Sidekiq only when a measured number — queue depth, pickup latency, jobs/min — says so. In the interview, that sentence with the numbers attached is the senior answer: they score the constraint-to-choice narration, not the pick.

§63 open

Open questions

  • Q1Does Graphiti ingestion need a queue at all yet — or does your existing 7:30 cron pattern cover it until mysecondbrain has a second user?
  • Q2What does GVL contention look like measured (Ruby 3.2+ GVL instrumentation, gvl_timing) on your actual workload, instead of assumed?
  • Q3If mysecondbrain ever hosts other people's vaults: does the tenant key travel in job arguments or in middleware — and what did InfoTech's architecture get wrong?
conceptual topology · this page only
limits requires broke at InfoTech chosen for background-jobs.md [[Graphiti]] [[mysecondbrain.work]] [[GIL]] [[Threads vs Processes]] [[Connection Pools]] [[Idempotency]] [[Multitenancy]] [[Solid Queue]]

solid border = note exists in your vault today · dashed = future entry, created when the pipeline sees you mention it again

revision history
  • rev 12026-07-18 09:12 — page seeded from live assessment (4 probes, 2 blank)
  • rev 209:19 — §2 rewritten around the GVL model + kitchen analogy
  • rev 309:27 — InfoTech memory linked; §3 decoded; retrieval scheduled → 2026-07-21