- Update Activity Log with 108 missing commits (48 backend + 60 frontend) - Update version references: backend v2.8.79, frontend v2.8.94 - Update migration count: 18 migrations (0000-0017) - Update Telegram Mini App Flow to v2.8.94 - Update Payment Flow - Scanner to 2026-06-05 - Update all architectural and database references - Add MongoDB removal handoff document with updated versions Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
7.4 KiB
issue, title, severity, domain, labels, status, created, source
| issue | title | severity | domain | labels | status | created | source | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| 136 | Backend: API profiling shows Mongo hot paths are index-backed but still do avoidable repeated reads | low | Performance |
|
open | 2026-05-31 | Live dev performance profiling 2026-05-31 |
Backend: API profiling shows Mongo hot paths are index-backed but still do avoidable repeated reads
Severity: low Domain: Performance Labels: performance, backend, mongodb, caching
Description
Live profiling on dev.manwe.qzz.io after the direct Caddy cutover showed the tested API routes are still Mongo-backed, not Postgres-backed. Postgres pg_stat_statements recorded no application SQL for these request windows; the only SQL activity came from profiling probes and database housekeeping.
The good news: the Mongo queries that did run were using indexes and were effectively sub-millisecond on the tiny dev dataset. The useful optimization work is therefore not "fix a slow scan" yet, but reducing repeated query fan-out and avoiding unnecessary reads before real data volume grows.
Profiling Snapshot
Environment:
- Backend
2.6.84 - Public entrypoint:
https://dev.manwe.qzz.iothrough Caddy -> local nginx -> backend - MongoDB
6.0.28, boundedsystem.profilecapture withslowms: 0 - PostgreSQL
18.4,pg_stat_statementsenabled - Docker block I/O deltas sampled per endpoint window
Representative results:
| Endpoint | Result | Mongo profile |
|---|---|---|
GET /api/marketplace/categories |
40 req, avg 296ms, p99 721ms | 1 total categories query across the window; Redis category cache is working |
GET /api/marketplace/categories/tree |
40 req, avg 319ms, p99 763ms | 40 categories queries, 24 docs/request, IXSCAN { isActive: 1 } |
GET /api/marketplace/sellers |
20 req, avg 306ms, p99 722ms | 1 users query/request, IXSCAN { role: 1 } |
GET /api/marketplace/request-templates/public/:shareableLink |
20 req, avg 315ms, p99 781ms | 3 queries/request: template by shareableLink, seller by _id, category by _id |
GET /api/payment/request-network/options?sellerId&templateId |
293 req, avg 270ms, p99 806ms | 2 queries/request: requesttemplates by _id, shopsettings by sellerId |
GET /api/addresses |
15 req, avg 315ms, p99 722ms | 1 addresses query/request, IXSCAN { userId: 1 } |
GET /api/marketplace/purchase-requests/my |
15 req, avg 314ms, p99 702ms | 3 queries/request: request list, request count, payment lookup |
POST /api/auth/login |
5 req, avg 691ms, p99 1058ms | user find + user updates; Mongo time rounded to 0ms, so latency is likely bcrypt/JWT/update path rather than query scan |
Disk write notes:
- Mongo profiler itself wrote about 1 MB in some windows; this is measurement overhead, not normal app write load.
- Scanner wrote small background chunks, about 17-24 KB, unrelated to the tested API routes.
- No meaningful app SQL write/read pressure appeared in the API windows.
Recommended Work
-
Cache
GET /api/marketplace/categories/tree.getAllCategories()already uses Redis viacacheService.getCategoryTree()re-queries Mongo every request even though it uses the same active category set.
-
Add a short TTL cache for resolved payment rails.
- Cache by
(sellerId, templateId)or a normalized key. - Invalidate on template payment config updates and shop settings updates.
- This removes two Mongo reads from every checkout options poll.
- Cache by
-
Avoid empty payment lookups in
getPurchaseRequestsByBuyer.- The profiled buyer had no matching requests, but the code still performed a
paymentslookup withpurchaseRequestId: { $in: [] }. - Short-circuit when the page result is empty.
- The profiled buyer had no matching requests, but the code still performed a
-
Add or confirm compound indexes before the dataset grows.
PurchaseRequest:{ buyerId: 1, createdAt: -1 }Address:{ userId: 1, primary: -1, createdAt: -1 }User: consider{ role: 1, isEmailVerified: 1 }for public seller lists.
-
Keep
pg_stat_statementsenabled and add a repeatable profiling script.- The dev server now has SQL statement profiling available.
nick-doc/scripts/profile-mongo-api.mjsruns the Mongo profiler around the same endpoint matrix and emits JSON/Markdown so future Postgres migrations can be compared cleanly.
Affected Files
backend/src/services/marketplace/CategoryService.ts:38— category tree bypasses the existing category cache.backend/src/services/payment/sellerPaymentConfig.ts:44— payment rail resolver performs per-call template/shop lookups.backend/src/services/marketplace/RequestTemplateService.ts:465— public template fetch fans out to template, seller, and category queries.backend/src/services/marketplace/PurchaseRequestService.ts— buyer list should skip downstream payment lookup when no request IDs exist.backend/src/services/address/addressController.ts:23— address list is index-backed; compound sort index should be confirmed.
Acceptance Criteria
categories/treeuses the same cached category source ascategoriesand invalidates on category writes.- Payment options with
sellerId+templateIdreturns from cache for repeated calls and invalidates on relevant seller/template settings changes. - Empty purchase request result pages do not issue a
paymentsquery. - Required compound indexes are present in model definitions or migration/startup index setup.
- A repeatable profiling command records per-endpoint latency, Mongo query groups, and block I/O deltas; SQL profiling remains available through the separate
pg_stat_statementspass.
Latest Repeatable Mongo Profile
Generated report:
09 - Audits/Mongo API Profiles/2026-05-31T14-26-19-969Z/summary.md09 - Audits/Mongo API Profiles/2026-05-31T14-26-19-969Z/mongo-api-profile.json
This pass used the script's RESET_BACKEND_LIMITER=1 option to restart the dev backend first. That clears the process-local express-rate-limit global limiter, so authenticated endpoint windows profile Mongo queries instead of stale 429 responses.
Clean run highlights:
| Endpoint | Requests | Non-2xx | Mongo ops | Top Mongo profile |
|---|---|---|---|---|
GET /api/marketplace/categories/tree |
10 | 0 | 10 | categories find, IXSCAN { isActive: 1 }, 24 docs/request |
GET /api/payment/request-network/options?sellerId&templateId |
50 | 0 | 100 | requesttemplates IDHACK + shopsettings by sellerId every request |
GET /api/addresses |
10 | 0 | 10 | addresses find, IXSCAN { userId: 1 }, sorted by primary and createdAt |
GET /api/marketplace/purchase-requests/my |
10 | 0 | 30 | list + count + unnecessary payments query with purchaseRequestId: { $in: [] } |
POST /api/auth/login |
5 | 0 | 15 | indexed user find plus user update operations; latency is outside Mongo query time |
Repeatable Mongo Profiling Command
cd nick-doc
RESET_BACKEND_LIMITER=1 \
BASE_URL=https://dev.manwe.qzz.io \
SSH_HOST=root@5.78.213.189 \
SSH_KEY=~/CascadeProjects/wzp \
node scripts/profile-mongo-api.mjs
The script enables Mongo system.profile only for each bounded endpoint window, groups captured query shapes by namespace/plan/query shape, writes a raw JSON profile and a Markdown summary under 09 - Audits/Mongo API Profiles/, then disables profiling again. Leave RESET_BACKEND_LIMITER unset when you do not want the script to restart the dev backend.
References
- Live profiling on
dev.manwe.qzz.io— 2026-05-31 - Caddy direct-path profiling pass after Pangolin/newt removal