How I Built a SaaS System That Handles 100,000 Users
How I Built a SaaS System That Handles 100,000 Users
Professional Guide by Alaa Amer - Expert Web Developer & Designer
Scaling a SaaS product from 0 to 100,000 active users is a different challenge from simply building one. This is a real walkthrough of the decisions, tradeoffs, and architecture that made it possible.
2️⃣ Multi-Tenancy: One Database or Many?
The biggest SaaS architecture decision. I chose shared database with tenant isolation (most cost-effective at scale).
-- Every table has a tenant_id column
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Composite index for performance
CREATE INDEX idx_projects_tenant ON projects (tenant_id, created_at DESC);
-- Row-Level Security for safety
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.current_tenant')::UUID);
Setting the tenant context per request:
// middleware/tenant.ts
export async function tenantMiddleware(req, res, next) {
const tenant = await resolveTenantFromHostname(req.hostname);
if (!tenant) return res.status(404).json({ error: "Tenant not found" });
// Set PostgreSQL session variable
await db.query(`SET LOCAL app.current_tenant = '${tenant.id}'`);
req.tenant = tenant;
next();
}
4️⃣ Caching Strategy with Redis
Cache aggressively — but cache the right things.
// lib/cache.ts
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
// Generic cache wrapper
export async function cached<T>(
key: string,
ttlSeconds: number,
fetcher: () => Promise<T>,
): Promise<T> {
const hit = await redis.get(key);
if (hit) return JSON.parse(hit) as T;
const value = await fetcher();
await redis.setex(key, ttlSeconds, JSON.stringify(value));
return value;
}
// Cache invalidation by pattern
export async function invalidatePattern(pattern: string) {
const keys = await redis.keys(pattern);
if (keys.length > 0) await redis.del(...keys);
}
Usage in a route:
// api/projects.ts
app.get("/api/projects", async (req, res) => {
const { tenantId } = req.tenant;
const projects = await cached(
`tenant:${tenantId}:projects`,
60, // 60 seconds TTL
() =>
db.query(
"SELECT * FROM projects WHERE tenant_id = $1 ORDER BY created_at DESC",
[tenantId],
),
);
res.json(projects.rows);
});
Cache layers I used:
- L1: In-memory (node-cache) — sub-millisecond, per-process
- L2: Redis — shared across all instances, milliseconds
- L3: CDN edge cache — for fully public, static responses
6️⃣ Auth, Rate Limiting, and Security
// middleware/auth.ts
import jwt from "jsonwebtoken";
export function authenticate(req, res, next) {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ error: "Unauthorized" });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET) as JWTPayload;
req.user = payload;
next();
} catch {
return res.status(401).json({ error: "Invalid token" });
}
}
// Rate limiting per tenant
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
export const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute window
max: 300, // 300 requests per minute per tenant
keyGenerator: (req) => req.tenant?.id ?? req.ip,
store: new RedisStore({ client: redis }),
handler: (req, res) => {
res.status(429).json({ error: "Rate limit exceeded. Please slow down." });
},
});
Summary
In this article we covered:
✅ Foundation and technology stack decisions ✅ Multi-tenancy with PostgreSQL Row-Level Security ✅ Database indexing and connection pooling ✅ Multi-layer caching with Redis ✅ Background job queues with Bull ✅ Auth, rate limiting, and security patterns ✅ Complete production-ready SaaS server skeleton
The biggest lesson: performance problems at scale are almost always solvable — but only if your architecture doesn't fight you. Build the right foundation early.
📩 Need help architecting your SaaS product?
Article Category
How I Built a SaaS System That Handles 100,000 Users
A real-world walkthrough of building a scalable SaaS architecture: multi-tenancy, database design, caching, queues, auth, monitoring, and lessons learned.
Consultation & Communication
Direct communication via WhatsApp or phone to understand your project needs precisely.
Planning & Scheduling
Creating clear work plan with specific timeline for each project phase.
Development & Coding
Building projects with latest technologies ensuring high performance and security.
Testing & Delivery
Comprehensive testing and thorough review before final project delivery.