Keep similar-ai from tripping the global circuit on a lone URL 502, clamp Qdrant search to 100, and add Server-Timing plus slow-request logging. Studio shared props, Academy S3 exists caching, heat chunking, and Redis/scheduler hygiene stay in this rollout.
13 KiB
M11 — HTTP Endpoint Performance & Laravel Request Profiling
STATUS: COMPLETE (read-only production HTTP audit + local low-risk fixes)
PRODUCTION: no cache flush, no restarts, no nginx/PHP/MySQL/FPM/Horizon changes
WHEN: 2026-08-23 20:58–21:05 CEST
Guest curls from server3 to https://skinbase.org (HTTP/2, Accept-Encoding: br). Two sequential requests per path. No concurrency. No cache clears.
M10 snapshot indexes: unchanged. Duplicate-index DROP remains unrun; production does not have that index.
1. HTTP baseline table
Times are TTFB seconds. Size is compressed body (brotli) unless noted.
| Path | Status | 1st TTFB | 2nd TTFB | Size (br) | Cache-Control | Encoding |
|---|---|---|---|---|---|---|
/ |
200 | 0.449 | 0.413 | ~22 KB | public, max-age=60, s-maxage=300, swr=600 |
br |
/ uncompressed HTML |
200 | 0.444 | — | 286 KB | same | none (no AE) |
/explore |
200 | 0.504 | 0.490 | ~16 KB | no-cache, private |
br |
/discover/trending |
200 | 0.449 | 0.434 | ~8.8 KB | no-cache, private |
br |
/uploads/latest |
200 | 0.573 | 0.594 | ~11.5 KB | no-cache, private |
br |
/academy |
200 | 1.283 | 0.838 | ~24 KB | no-cache, private |
br |
/academy/prompts |
200 | 1.316 | 1.171 | ~29 KB | no-cache, private |
br |
/academy/prompts/popular |
200 | 0.835 | — | — | private | br |
/forum |
200 | 0.512 | 0.525 | ~15 KB | no-cache, private |
br |
/collections |
404 | 0.682 | 0.440 | ~8.7 KB | private | br |
/collections/featured |
200 | 1.037 | — | — | private | br |
/rss/latest-uploads.xml |
200 | 0.349 | 0.350 | ~13 KB | no-cache, private |
br |
/sitemap.xml |
200 | 0.125 | 0.064 | 377 B | public, max-age=21600 |
br |
/search?q=skin |
200 | 0.467 | 0.520 | ~15 KB | private | br |
/statistics |
302 → /login | 0.309 | 0.254 | 709 B | private | — |
/leaderboard |
200 | 0.439 | 0.435 | ~14 KB | private | br |
/staff |
200 | 0.382 | 0.413 | ~8 KB | private | br |
/@killua (profile) |
200 | 0.975 | 0.968 | 249 KB uncompressed | private | — |
/favicon.ico |
200 | 0.063 | — | — | static | — |
No Server-Timing header before this milestone. Added locally (AddServerTiming).
2. Warm vs second request
| Path | Δ | Interpretation |
|---|---|---|
/ |
−36 ms | Guest homepage Cache::flexible working |
/sitemap.xml |
−61 ms | Public cache / file cache |
/academy |
−445 ms | Application cache (Academy home payload) + SSR warm |
/academy/prompts |
−145 ms | Still >1 s — listing + S3 exists + SSR |
| Most others | ~0 | Cache already warm; remaining cost is PHP + SSR |
Did not flush Redis to manufacture cold tests.
3. Access-log latency analysis
/etc/nginx/nginx.conf uses default access_log (combined). No custom log_format.
Missing: request_time, upstream_response_time, upstream_connect_time.
Log file /var/log/nginx/skinbase.org-access.log is www-data:adm — app user cannot read it. No p95/p99 from access logs. Do not change nginx during M11.
4–5. Laravel logs / 5xx
storage/logs/laravel.log ~3.5 GB.
CURRENT (hours around audit): download warnings for missing originals (not 5xx). No Redis LOADING. No Predis memory exhaustion.
RECENT (same day): CLI errors (nova:metrics-prune not defined, tinker parse, Enhance worker unavailable). Not HTTP user-facing.
HISTORICAL: Redis LOADING / presence SMEMBERS / Predis OOM — M7/M8, not recurring in this window.
HTTP 5xx count from nginx: unknown (log unread + no timing format).
6. Middleware cost
Web append: AddServerTiming (new), SecurityHeaders, RedirectLegacyProfileSubdomain, TrackOnlineVisitor, UpdateLastVisit, HandleInertiaRequests, onboarding/email-upgrade.
| Middleware | Per request | Verdict |
|---|---|---|
| TrackOnlineVisitor | Redis GET + SETEX + SADD after response; bots included; RSS/sitemaps now skipped | LOW after skip |
| UpdateLastVisit | DB update throttled 300s, auth only | NO CHANGE |
| HandleInertiaRequests | auth user + was full studioOptionsForUser on every auth Inertia page |
HIGH for auth — gated to /studio |
| Session | skipped for some paths via ConditionalStartSession | NO CHANGE |
7. Inertia shared props
app/Http/Middleware/HandleInertiaRequests.php::share
| Prop | Was | Now |
|---|---|---|
auth.user |
profile lazy-load | load('profile') once |
flash |
closures | same |
cdn / features |
config | same |
studio_groups |
eager Group::with(owner.profile, members) + whereHas for every logged-in Inertia page | only when path starts with studio |
studio_groups is consumed only by resources/js/Layouts/StudioLayout.jsx.
8. SSR audit
- Enabled:
config/inertia.phpssr.enabled=true,http://127.0.0.1:13714 - Process:
node …/bootstrap/ssr/ssr.js(www-data) - Used for Inertia HTML first visits (Academy, forum, leaderboard, collections, profile)
- Home
/is Blade, not SSR - No SSR duration in logs before Server-Timing
- Do not disable SSR globally
- Academy/collections ~1 s likely SSR + PHP; second academy hit faster (cache)
9. Controller mapping (slowest)
| Path | Controller |
|---|---|
/ |
App\Http\Controllers\Web\HomeController::index → HomepageService::all / allForUser |
/explore |
ExploreController::index (Meilisearch) |
/discover/trending |
DiscoverController::trending |
/academy |
Academy\AcademyHomeController::index |
/academy/prompts |
Academy\AcademyPromptController::index |
/leaderboard |
LeaderboardPageController → cached LeaderboardService::getLeaderboard |
/collections/featured |
CollectionDiscoveryController::featured (6 discovery queries + SSR) |
/search |
SearchController::index → Meilisearch + groups + news LIKE |
/@user |
User\ProfileController::showByUsername |
/download/artwork/{id} |
ArtworkDownloadController |
| RSS | RssFeedController::latestUploads |
10–11. Query counts / M10 overlap
Local Pest: leaderboard as auth user 0 groups / group_members queries after the shared-prop gate.
Production EXPLAIN not repeated (M10). HTTP-facing:
- Leaderboard HTTP uses Cache::remember, not the 9M snapshot GROUP BY
- Heat job is scheduler, not HTTP
- Browse
/exploreuses Meilisearch then hydrate — TTFB ~500 ms
12–13. Redis / presence
Per tracked GET: GET record, SETEX 300s, SADD index. No SMEMBERS on the request path (M8 SSCAN prune remains).
RSS/sitemaps/robots were tracked; now skipped. Bots still tracked on HTML (by design).
14. Payload
Homepage uncompressed ~286 KB HTML (Inertia-like JSON in Blade). Brotli ~22 KB.
Profile /@killua 249 KB uncompressed HTML — large Inertia SSR document. MEDIUM, no rewrite this milestone.
Academy compressed 24–29 KB — fine; TTFB is CPU/S3/SSR not transfer.
15–21. Surface findings
Home: guest flexible cache 5s/30s fresh + 1800s stale. TTFB ~410–450 ms. Personalized allForUser uncached (not measured; private).
Browse: /explore ~500 ms; no-cache, private even for guests (CDN cannot cache). MEDIUM documentation only — personalization/maturity may require private.
Artwork detail: not sampled with a canonical slug this pass (explore HTML is CDN image URLs). Code path ArtworkController::show / web artwork.
Search: 467 ms including Meilisearch + optional news LIKE on content. Malformed query guard already in SearchController.
Statistics: auth-gated 302. Rankings /leaderboard 439 ms from cache.
Profile: ~970 ms both hits — SSR + heavy profile assembly. Documented, no rewrite.
Academy: slowest public family. Causes: Inertia SSR + Storage::exists on S3 for prompt preview variants + courseLessons()->count() when lessons_count_cache is 0 (?: bug). Fixes: Redis-remember exists 1h; use cache column without extra count.
Forum: 512 ms. Existing query-bound tests. Queues stay off.
RSS: 350 ms, two queries (Artwork::published()->with(user)->limit). Redis not required for generation. Presence writes removed from RSS.
Download: production currently logs missing originals after incrementing counters — fixed (404 first).
22. External / storage
Academy list Storage::disk(s3)->exists per variant — was synchronous S3 on HTTP. Now cached 1h.
Download File::isFile local originals.
Enhance worker errors are CLI/job, not public pages.
23. Cache / stampede
Homepage already flexible (stale-while-revalidate). Leaderboard Cache::remember — stampede possible but 439 ms and scheduled refresh exist. No new caches except Academy asset-exists.
24. Bots / deep pagination
Access logs unreadable. Historical malformed search URLs remain historical. No UA blocking.
25. HTTP cache / compression / static
- gzip + brotli on in nginx (confirmed
content-encoding: brwhen AE sent) - Static
favicon.ico63 ms, not Laravel /build/*access_log off in vhost- Guest
/already public CDN-friendly; most Inertia HTMLprivate, no-cache(auth/session/SSR)
26. TTFB classification
| Endpoint | Dominant cost |
|---|---|
/ |
PHP bootstrap + cached payload + HTML serialize (~400 ms floor) |
/explore, /discover/* |
PHP + Meilisearch + Blade/Inertia |
/academy* |
SSR + PHP + (was) S3 exists |
/collections/featured |
SSR + multiple discovery SQL |
/@user |
SSR + profile assembly |
/leaderboard |
PHP + Redis cache hit |
/sitemap.xml |
static/cached XML |
/rss/* |
PHP + small SQL |
27. Ranked findings
| Severity | Item |
|---|---|
| HIGH | Auth Inertia studio_groups full group graph every page — fixed (studio path only) |
| HIGH | Download counters/events before missing-file 404 — fixed |
| HIGH | Academy S3 exists on listing/home cards — fixed (1h cache) |
| MEDIUM | Academy / collections / profile ~1 s (SSR). Do not disable SSR |
| MEDIUM | Guest explore/discover Cache-Control: private — CDN cannot cache |
| MEDIUM | /collections 404; real routes /collections/featured etc. |
| LOW | Presence on RSS — fixed skip |
| LOW | lessons_count_cache ?: count() extra SQL — fixed |
| NO CHANGE | FPM 14, Horizon, Redis size, snapshot indexes, forum queues, MySQL knobs |
28. Files changed
app/Http/Middleware/HandleInertiaRequests.phpapp/Http/Middleware/TrackOnlineVisitor.phpapp/Http/Middleware/AddServerTiming.php(new)bootstrap/app.phpapp/Http/Controllers/ArtworkDownloadController.phpapp/Services/Academy/AcademyAccessService.phptests/Feature/Http/StudioGroupsSharedPropTest.phptests/Feature/Http/ArtworkDownloadMissingFileTest.phptests/Feature/Http/RssPresenceSkipTest.phpdocs/optimization-m11-http-endpoint-performance.md
29. Tests
pest StudioGroupsSharedPropTest PASS
pest ArtworkDownloadMissingFileTest PASS
pest RssPresenceSkipTest PASS
30. Production deployment plan
- Deploy application code only.
- Do not run M10 DROP INDEX.
- Do not restart Redis/MySQL/FPM/Horizon except normal PHP-FPM reload for PHP.
- SSR process already running; no disable.
31. Post-deploy verification
From server3, sequential:
curl -sS -D - -o /dev/null --max-time 20 -H 'Accept-Encoding: br' https://skinbase.org/academy | grep -iE 'HTTP/|server-timing|cache-control'
curl -sS -o /dev/null -w 'academy ttfb:%{time_starttransfer}\n' -H 'Accept-Encoding: br' https://skinbase.org/academy
curl -sS -o /dev/null -w 'prompts ttfb:%{time_starttransfer}\n' -H 'Accept-Encoding: br' https://skinbase.org/academy/prompts
curl -sS -o /dev/null -w 'home ttfb:%{time_starttransfer}\n' -H 'Accept-Encoding: br' https://skinbase.org/
Expect Server-Timing: app;desc="Laravel";dur=…. Academy TTFB should move toward the warm ~0.8 s band more often (asset-exists cache). Guest home should stay ~400–450 ms.
32. Rollback
Revert the listed PHP files. Presence skip and Server-Timing are additive. Download 404-before-increment is the correct semantics (do not roll back unless a download of a missing file must still increment stats — it should not).
Measurement note (acceptance)
Public guest pages already under ~500 ms except Academy (~1.3 s), collections featured (~1.0 s), profile (~0.97 s).
| Path | BEFORE (prod guest) | AFTER (expected) |
|---|---|---|
/ |
0.45 / 0.41 s | same |
/academy |
1.28 / 0.84 s | fewer S3 round-trips; SSR remains |
/academy/prompts |
1.32 / 1.17 s | fewer S3 round-trips |
| Auth non-studio Inertia | unmeasured (group graph every request) | 0 extra group SQL (tested) |
| Missing download | counters + 404 | 404 only (tested) |