A tuning playbook for B2B portals handling high-concurrency sessions across search, checkout, and reporting-heavy workflows.
B2B portals fail under load in ways that consumer apps don't. A consumer e-commerce site optimizes for a user who browses a few products and adds one to cart. A B2B portal optimizes for a procurement officer who filters 80,000 SKUs by 12 attributes, exports the result to Excel, and concurrently runs a 4,000-line order. Different problem, different tuning sequence.
This playbook is the tuning sequence we follow when a B2B portal starts hitting p95 latency walls. It assumes a Next.js + Node API + Postgres + Elasticsearch stack but the sequence is mostly stack-agnostic.
Step 0 — measure before you change anything
You need three things before touching code: real RUM data (Web Vitals from actual users), backend distributed traces (OpenTelemetry into Tempo or Honeycomb), and a synthetic load test that reproduces the actual workload shape (we use k6 with scripts that mirror top-10 user journeys). Tuning without measurement is gambling.
Bottleneck map
Step 1 — reduce over-fetching
List views are the #1 culprit. The typical PR we see fetches a 50-field product object to render a card that shows 6 fields. Fix:
- Add explicit list-view DTOs (don't return the full entity from a list endpoint).
- Use GraphQL or REST `?fields=` projection on the API; reject queries without projection on high-volume endpoints.
- Server-side paginate; do not return more than 100 items per page even if the UI "needs" them all (use infinite scroll or virtualized lists).
Typical impact: 40–70% payload reduction, 30–50% latency reduction on list endpoints.
Step 2 — separate SQL and search
Postgres is great for transactional reads and bad at full-text relevance ranking on 80k+ records with 12 filters. Stop using `LIKE '%term%'` queries on the main DB. Move search to Elasticsearch/OpenSearch with a sync pipeline (CDC into search, async).
Once split, tune them separately. Postgres gets covering indexes for the filters that hit the main tables; Elasticsearch gets a relevance model tuned with click data.
Step 3 — selective caching
Cache everything that can be stale and invalidate aggressively on writes. Concrete defaults:
- Catalog metadata (categories, brands): 1-hour edge cache, purge on admin update.
- Product detail: 5-minute edge cache, vary on customer-pricing key, purge on stock/price change.
- Search results: 30-second edge cache, vary on full query+facets+customer-tier hash. This single change typically takes 60% load off search.
- Customer-specific data (cart, orders): no cache.
Use Cache-Control + stale-while-revalidate on the CDN. Don't try to invalidate everything precisely — a short TTL + key versioning is more reliable than a clever invalidation scheme.
Step 4 — cap the worst queries
Find the top 10 slowest SQL queries by total time (not per-call) in production. These almost always include a missing index or a SELECT * inside a loop. Fix in priority order — the top 3 usually account for 50% of DB CPU.
Tools: `pg_stat_statements` is sufficient for Postgres. For ORM-heavy codebases, log query plans for slow queries in non-prod with the same data shape.
Step 5 — concurrency limits and backpressure
High-concurrency portals fail catastrophically without limits. Set:
- Per-tenant rate limits at the gateway.
- Connection pool limits on every database client (don't trust defaults).
- Bulk operation queues with explicit concurrency caps; reject or queue when the cap is hit, don't silently retry.
This is the difference between a portal that gets slow and one that goes down.
Closing
Performance tuning is a sequence, not a sprint. Measure, fix the worst three things, re-measure, repeat. The wins compound and the next bottleneck always reveals itself.