惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Visual Studio Blog
IT之家
IT之家
博客园 - 聂微东
The Cloudflare Blog
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
酷 壳 – CoolShell
酷 壳 – CoolShell
爱范儿
爱范儿
H
Help Net Security
博客园 - 叶小钗
V
V2EX
WordPress大学
WordPress大学
J
Java Code Geeks
Hugging Face - Blog
Hugging Face - Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园_首页
C
Check Point Blog
B
Blog
D
DataBreaches.Net
美团技术团队
罗磊的独立博客

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Your SQL Is Fast but the API Is Slow: It's the Ruby Layer
Dane Wu · 2026-06-28 · via DEV Community

Rails Performance: Lessons from Production — #6

By now the DB queries, caching, and background work are handled. But sometimes the SQL is genuinely fast and the API is still slow, the CPU still high — because the bottleneck moved into Ruby itself: heavy serialization, allocating too many objects, recomputing the same thing. This post is about the cost in the application layer, after the request reaches the controller. Same example throughout (a {% raw %}shipments table).


💥 SQL took 30ms, the API took 800ms

A shipments-list API. The APM showed SQL was only 30ms, but the whole request took 800ms. N+1 and indexes were already checked — the DB was fine. The slow part was "what Ruby does after the data comes back."

That remaining 770ms went to three common places: bloated serialization, too many intermediate objects, and the same computation running repeatedly. Let's take them one at a time.


📦 Trap 1: serializing a pile of fields nobody uses

The most common one — the API turns the whole model into JSON:

render json: @shipments   # every field of every shipment gets dumped

Problems:

  • A shipment has 40 columns, the frontend uses 5 — serializing the other 35 × thousands of rows is pure waste.
  • You might also leak sensitive fields (internal cost, API keys) by accident.

The fix: return only the fields you need. Spell them out:

render json: @shipments.as_json(only: [:id, :tracking_no, :status])

Serialization is pure CPU work — fewer fields × more rows, more saved.

Two things worth adding:

  • Safety should come from a structural whitelist, not from remembering only:. as_json dumps all fields by default, and only: is a manual whitelist the next person easily rewrites back to render json: @shipments. To make "new fields don't leak by default" a structural guarantee, use a serializer (Alba / blueprinter / AMS) that pins the visible schema.
  • as_json(only:) saves serialized fields, but doesn't save "instantiating every row into an AR object + building a Hash per row" — which is exactly the cost in Trap 2. When fields are few but rows are huge and you want to skip instantiation too, use pluck(:id, :tracking_no, :status) to fetch arrays and never build AR objects at all.

♻️ Trap 2: allocating too many intermediate objects in a hot path (GC pressure)

Every object Ruby creates costs memory; lots of allocation increases GC (garbage collection) work and grows the heap, which drags throughput once it scales. (MRI's GC is generational + incremental, so short-lived objects are actually relatively cheap — the truly expensive thing is "many, long-lived, growing the heap." So the point is: don't allocate pointlessly in a hot path.) It shows up most on a path that runs "for every row":

# ❌ each shipment builds a new hash, a new string, runs a format
@shipments.map do |s|
  { label: "#{s.tracking_no} - #{s.status.upcase} - #{format_date(s.created_at)}" }
end

Thousands of rows × several intermediate objects each = tens of thousands of short-lived objects thrown at the GC.

The fix: allocate fewer intermediates in the hot path — push what the DB can compute to the DB (back to #3), merge steps, and don't rebuild the same thing inside the loop.

# pull the invariant out of the loop, don't rebuild it each iteration
status_labels = { "delivered" => "Delivered", ... }   # built once
@shipments.map { |s| status_labels[s.status] }

Concept: object allocation has a cost. One or two don't matter, but amplified by "every row × every request" the GC becomes a hidden bottleneck.


🧮 Trap 3: the same computation, run many times in one request

def total_weight
  shipments.sum(&:weight)   # recomputes on every call
end
# in one request, the view, a helper, and the serializer each call it → computed three times

The fix: memoization — compute once, reuse within the same request.

def total_weight
  @total_weight ||= shipments.sum(&:weight)   # compute first time, then just return it
end

@x ||= ... means "compute only when @x is nil or false, otherwise return it." Within the same object, in the same request, this runs once.

⚠️ The ||= trap: it tests "is the value nil/false," not "has it been computed." If the result is legitimately nil or false (e.g. a lookup that returns nil), every call recomputes and memoization is effectively dead — a hidden perf bug on an expensive query. For that, write @x = compute unless defined?(@x). (sum returns a number so it dodges this, but you should know the general gotcha.)

Also: memoization works "within one request" because the object being memoized on is itself built per-request (GC'd when the request ends) — not because the mechanism is request-scoped. Memoize on a class-level / singleton / long-lived service object and it persists across requests, causing stale data or memory leaks. Be careful.


🏁 Wrap-up

When the DB is already fast but the API is still slow, the bottleneck is usually in the Ruby layer:

trap symptom fix
bloated serialization returning fields nobody uses return only what's needed (only: / a serializer / pluck)
too much allocation hot path builds many intermediates, high GC allocate fewer intermediates, push to the DB
repeated computation same computation many times per request memoization (`@x

One principle:

DB optimization is "ask the database for less"; application-layer optimization is "do less busywork after the data comes back."

Each of these looks like "saves a tiny bit" on its own, but they share one amplifier — "every row × every request." One field, one object, one computation, times thousands of rows and thousands of requests, is exactly those vanished hundred-odd milliseconds. Confirm with an APM that it's genuinely the Ruby layer (SQL fast, request slow) before digging in here.