Overview
Hostwares runs your containers on fast infrastructure with automatic TLS and global routing, but application-level choices dominate real-world latency. This guide covers framework-agnostic techniques to keep p50 low and p95 stable.
| Lever | Impact | Effort |
|---|---|---|
| Cache headers & CDN | High — cuts TTFB for repeat views | Low — add headers |
| Image & asset optimization | High — reduces LCP | Low — build-time compression |
| Runtime scaling | Medium — smooths p95 under load | Medium — tune pack / concurrency |
| DB indexing & pooling | High — fixes slow queries | Medium — analyze & migrate |
| Observability | Enabler — find the bottleneck | Low — add logs & metrics |
What to Measure
Track these before and after each optimization:
| Metric | What It Tells You | Target |
|---|---|---|
| TTFB | Server + network latency | < 200 ms (cached), < 600 ms (dynamic) |
| LCP | Largest contentful paint | < 2.5 s |
| CLS / INP | Visual stability & interactivity | CLS < 0.1, INP < 200 ms |
| p95 latency | Tail latency under load | Within 2× p50 |
| Error rate | 5xx / timeouts | < 0.1% |
# Quick checks
curl -w "TTFB: %{time_starttransfer}s Total: %{time_total}s\n" -o /dev/null -s https://your-site.hostwares.app/
# Lighthouse CI (any framework)
npx lighthouse https://your-site.hostwares.app/ --only-categories=performance --view
# k6 load test
import http from 'k6/http';
export default function () { http.get('https://your-site.hostwares.app/'); }Caching
Cache at the right layer — browser, CDN, and application — with explicit headers.
HTTP cache headers
# Static assets — immutable, long-lived
Cache-Control: public, max-age=31536000, immutable
# HTML / dynamic — revalidate often
Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=300
# Private / user-specific — never cache at edge
Cache-Control: private, no-store// Framework-agnostic middleware example
function withCacheHeaders(res, type) {
if (type === "static") {
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
} else if (type === "html") {
res.setHeader("Cache-Control", "public, max-age=0, s-maxage=60, stale-while-revalidate=300");
} else if (type === "private") {
res.setHeader("Cache-Control", "private, no-store");
}
}
// Nginx
location ~* \.(js|css|png|jpg|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}Application cache
// In-memory (per container) — good for hot keys
const cache = new Map();
async function getUser(id) {
if (cache.has(id)) return cache.get(id);
const user = await db.users.findUnique({ where: { id } });
cache.set(id, user);
setTimeout(() => cache.delete(id), 60_000);
return user;
}
// Shared — Redis / Upstash (works across containers)
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
await redis.set("user:" + id, JSON.stringify(user), "EX", 60);CDN & Edge
Put a CDN in front of your Hostwares site to cache static assets and absorb traffic spikes. Any CDN works — Cloudflare, Fastly, CloudFront.
- Point your domain to Hostwares, then proxy through your CDN (orange cloud / CDN enabled).
- Cache static paths at the edge; bypass cache for
/api/*and authenticated routes. - Use
s-maxageandstale-while-revalidateto keep edge hits high without stale HTML.
# Cloudflare cache rule example (via dashboard or API)
# Cache static, bypass API
# Rule 1: (http.request.uri.path contains "/_next/static" or ends with .js/.css/.png)
# -> Cache: Eligible, Edge TTL 1 month, Browser TTL 1 year
# Rule 2: (http.request.uri.path contains "/api/")
# -> Cache: Bypass| Path Pattern | Edge Behavior | Header to Set |
|---|---|---|
/_next/static/*, *.js, *.css | Cache 30d | public, max-age=31536000, immutable |
/images/*, /assets/* | Cache 7d | public, max-age=604800 |
/, /blog/* | Cache 60s + SWR 300s | s-maxage=60, stale-while-revalidate=300 |
/api/*, /dashboard/* | Bypass | private, no-store |
Images & Assets
Images are usually the biggest LCP contributor — optimize at build and serve time.
- Compress and resize at build — serve
webp/avifwith fallbacks. - Use responsive images (
srcset/sizes) so mobile does not download desktop-sized files. - Lazy-load below-the-fold images; preload the LCP image.
- Bundle-split and tree-shake — ship only JS the route needs.
<!-- Responsive, lazy, modern format -->
<img
src="/images/hero-800.webp"
srcset="/images/hero-400.webp 400w, /images/hero-800.webp 800w, /images/hero-1200.webp 1200w"
sizes="(max-width: 640px) 100vw, 800px"
width="800" height="450"
loading="lazy" decoding="async"
alt="Hero"
/>
<!-- Preload LCP image -->
<link rel="preload" as="image" href="/images/hero-800.webp" imagesrcset="/images/hero-800.webp 800w" />
# Build-time compression (any stack)
npx sharp -i ./public/images -o ./public/images --webp --avif
# or: squoosh, imageoptim, vite-imagetoolsRuntime & Scaling
Hostwares runs each site in dedicated containers. Tune concurrency and resources to match your workload.
| Knob | When to Adjust | How |
|---|---|---|
| Pack size (CPU/RAM) | OOM, high p95, build timeouts | Dashboard → Site → Settings → Resources |
| Concurrency / workers | CPU-bound or high-throughput APIs | WEB_CONCURRENCY / cluster mode |
| Health check | Slow cold starts | Increase timeout; add /api/health |
| Keep-alive | Many short requests | Enable HTTP keep-alive in server |
# Node: cluster via WEB_CONCURRENCY
# Procfile / start command
web: node --max-old-space-size=1536 server.js
# Set WEB_CONCURRENCY=2..4 on larger packs
# Python: gunicorn workers
gunicorn app.wsgi --workers 3 --threads 2 --timeout 30
# Generic: respect PORT and graceful shutdown
const port = process.env.PORT || 3000;
process.on("SIGTERM", () => server.close(() => process.exit(0)));For sustained traffic growth, increase the pack before adding app-level caching — memory pressure causes GC pauses that hurt tail latency more than average latency.
Database
A slow query will dominate any edge optimization. Fix the DB first.
- Index frequently filtered/sorted columns; use
EXPLAIN ANALYZEto verify. - Use connection pooling — direct connections exhaust quickly under concurrency.
- Paginate large result sets; avoid
SELECT *over wide tables.
-- Find slow queries
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 'u_123' ORDER BY created_at DESC LIMIT 20;
-- Add index if missing
CREATE INDEX CONCURRENTLY idx_orders_user_created ON orders(user_id, created_at DESC);
-- Pooled connection (Pgbouncer)
DATABASE_URL=postgresql://user:pass@host:6543/db?pgbouncer=true
-- App: always limit
-- const orders = await db.orders.findMany({ where: { userId }, orderBy: { createdAt: "desc" }, take: 20 });Observability
Use logs and metrics to prove where time is spent.
- Deployment logs — check build time and image size (large images slow cold starts).
- Runtime logs — add structured request logging with duration.
- Resource metrics — watch memory and CPU in Site → Metrics; alert on sustained > 80%.
// Structured request log (any framework)
app.use((req, res, next) => {
const start = Date.now();
res.on("finish", () => {
console.log(JSON.stringify({
method: req.method,
path: req.path,
status: res.statusCode,
durationMs: Date.now() - start,
}));
});
next();
});
// Query logs via AI
// "Show me the slowest requests in the last hour for my-site"
// "Are there any 5xx errors after the last deploy?"Performance Checklist
- ✅ Static assets have
immutablecache headers and are on a CDN - ✅ HTML uses
s-maxage+stale-while-revalidate, notno-storeglobally - ✅ Images are compressed, responsive, and lazy-loaded (LCP preloaded)
- ✅ Pack sized for workload; no recurring OOM or CPU throttling
- ✅ DB has correct indexes and uses pooled connections
- ✅ Request logging enabled; p95 and error rate monitored
- ✅ Rollback tested — can revert a bad deploy in under a minute