Saeed Ghofrani
For RecruitersArchitectureCase StudiesProjectsExperienceSkillsBlogContact
Discuss a role
For RecruitersArchitectureCase StudiesProjectsExperienceSkillsBlogContact
  1. Home
  2. /Blog
  3. /How I Think About Backend Performance

Hiring for backend ownership?

The recruiter brief has my role fit, strongest production results, resume, and direct contact details.

Open recruiter briefEmail me

Saeed Ghofrani Ivari

I build backend systems, lead delivery, and stay close to production.

Focus

NestJS, data-heavy services, real-time products, and Linux operations.

Contact

sa.ghofraniivari@gmail.comTelegram
GitHubLinkedInStack OverflowDev.toRecruiter briefPlayground
backendperformancepostgresqlredis

How I Think About Backend Performance

A practical field guide for improving API latency without breaking business behavior or hiding cost in another layer.

3 min readField notesSaeed Ghofrani Ivari

From my notebook

A repeatable way to investigate a slow endpoint

Where this came from

This is the checklist I use before changing a slow NestJS or PostgreSQL path. It grew out of production work where bounded reads, query cleanup, and carefully owned caching reduced average API latency by 40%. It is a working method, not a benchmark report.

My short checklist

  • - Map the full request path before changing code.
  • - Prefer bounded reads and explicit selects over broad relation loading.
  • - Use Redis only when invalidation and ownership are clear.
  • - Move slow work to queues only when retry and idempotency rules exist.

Performance work starts before the profiler. I first map the request path: controller, service, repository, external calls, cache lookups, database queries, serialization, and response size. That map shows where time can be spent and where a change can accidentally alter behavior. In production systems, the dangerous performance fixes are the ones that look small but quietly change data shape, ordering, authorization, or pagination semantics.

Start with the user-visible path

The useful question is not "is PostgreSQL slow?" The useful question is "which user action is slow, for which data shape, under which constraints?" A dashboard query with ten rows has a different failure mode than an export endpoint, a chat inbox, or an operator search screen. I capture the endpoint, payload size, filters, sort order, current latency, and the business rule the response must preserve.

Make reads bounded and intentional

Most slow API paths I see come from broad reads. An ORM include graph grows over time because each new feature adds one more relation. Eventually a simple list endpoint loads data for a detail page, admin page, notification badge, and historical audit all at once. I prefer scoped repository methods with explicit select fields, stable limits, and separate detail endpoints when the UI truly needs deeper data.

  • Use select before include when the response shape is known.
  • Keep pagination based on page size, cursor, or bounded offset instead of limit multiplied by page.
  • Add indexes only after checking the real filter and order pattern.
  • Measure before and after with the same input shape.

Cache only after ownership is clear

Redis can hide waste, but it can also create stale behavior that is harder to debug than the original slow query. I cache data when the owner, invalidation event, TTL, and fallback behavior are obvious. If invalidation is vague, I usually fix the query first. A cache should reduce repeated cost, not become a second source of truth.

Move slow work out of the request

Some work does not belong in the request path: sending emails, generating reports, syncing external APIs, processing files, or calling slow third-party services. Queues help when the product can accept eventual completion and when retry/idempotency rules are defined. Without those rules, async work only moves the bug to a worker.

The final step is verification. I do not call a performance change finished until the endpoint still returns the same shape, the slow case is measurably faster, and the operational risk is understood. Good performance work is boring in the best way: fewer rows read, fewer bytes returned, fewer repeated calls, and no surprise behavior changes.

Bounded Prisma reads with explicit fields keep API payloads predictable.
const orders = await prisma.order.findMany({
  where: { customerId, status: { in: ["open", "paid"] } },
  select: {
    id: true,
    status: true,
    total: true,
    createdAt: true,
  },
  orderBy: { createdAt: "desc" },
  take: 50,
});

Who may find it useful

Backend engineers, tech leads, and product teams debugging slow APIs

Topics

PostgreSQL query plansPrisma select patternsRedis TTL and invalidationRabbitMQ worker retries