Back to Blog
Architecture

Choosing a Tech Stack for High-Load Applications

A defensible default stack, layer by layer, with the reasoning and the alternatives — and the constraints that should actually drive the choice.

Published
Reading time
12 min read
Author
Yakhya

There is no best stack, only stacks that fit a set of constraints. But "it depends" is a useless answer, so here is a concrete default I would defend for a system expected to handle tens of thousands of requests per second, along with the reasoning and the conditions under which I would choose differently.

Services: Go, with Java or Rust at the edges

Go is the pragmatic default for high-throughput services: cheap goroutines instead of thread-per-request, predictable sub-millisecond GC pauses, static binaries that make containers trivial, a strong standard library, and — importantly — a small enough language that a team of ten writes code that looks the same. Java (or Kotlin) on a modern JVM with virtual threads is equally defensible and often better where you need a mature ecosystem, complex domain logic, or existing expertise. Rust is the right call for the genuinely hot path where you need C-level performance without the memory-safety risk, but its learning curve makes it a poor blanket choice. Node.js and Python remain excellent for I/O-bound edge services and anything ML-adjacent, and a poor fit for CPU-bound cores.

Data: PostgreSQL first, specialized stores when proven

Start with PostgreSQL and stay there far longer than instinct suggests. Properly indexed, connection-pooled through PgBouncer, with read replicas and partitioned large tables, it handles a workload most teams will never exceed. It gives you real transactions, JSONB when you need schema flexibility, and an ecosystem of extensions. Reach past it deliberately: Cassandra or ScyllaDB for write-heavy time-series at massive scale, ClickHouse for analytical queries over billions of rows (it is genuinely transformative there), Elasticsearch or OpenSearch for full-text and faceted search, and a managed distributed SQL option like CockroachDB or Vitess when you have truly outgrown a single writer.

  • Redis (or Valkey) for caching, rate limiting, leaderboards, and ephemeral state. Cluster mode when one node's memory is no longer enough.
  • Kafka as the event backbone once you have several services that need durable, replayable streams; RabbitMQ or NATS if you mainly need task queues and want far less operational weight.
  • Object storage (S3 or compatible) for anything large. Never store files in the database.
  • A CDN in front of everything static, and in front of cacheable API responses too — the cheapest request is the one that never reaches you.

Edge and traffic management

Cloudflare or an equivalent for DDoS protection, TLS termination, and static caching. An API gateway (Envoy, Kong, or the cloud provider's) for routing, authentication, and rate limiting, so those concerns are not reimplemented in every service. gRPC internally for typed contracts and streaming; REST or GraphQL at the public boundary. Rate limit at the edge and again per-service — the edge protects the platform, the service protects itself from its own noisy neighbours.

Runtime and delivery

  • Kubernetes (managed — EKS, GKE, AKS) once the service count justifies it; a managed container platform such as ECS, Cloud Run, or Fly.io below that line.
  • Terraform or Pulumi for infrastructure, in the same review process as application code.
  • GitOps with Argo CD or Flux, so the cluster's state is the repository's state.
  • Trunk-based development, feature flags, canary deploys, and an automated rollback. Deploy frequency is a reliability feature, not a risk.

Observability, which is not optional at this scale

OpenTelemetry for instrumentation; Prometheus with Thanos or Mimir for metrics; Loki, Elasticsearch, or a vendor for logs; Tempo or Jaeger for traces; Grafana over all of it. Whatever you pick, decide it before launch — retrofitting instrumentation into a system already on fire is the worst possible time to do it.

The stack matters less than the boundaries. A well-partitioned monolith on boring technology outperforms a distributed system whose services share a database.

How to actually decide

  1. 01Write the numbers down: requests per second at peak, data volume in three years, latency budget, consistency requirements, compliance constraints.
  2. 02Weight your team's existing expertise heavily. An unfamiliar stack costs you a year, and that year is usually the year that decides the product.
  3. 03Prefer boring, widely deployed technology. You want the problem you hit at 2am to have a Stack Overflow answer from 2019.
  4. 04Count the operational cost of each component honestly — self-hosting Kafka, Elasticsearch, or a database is a full-time responsibility, not a line item.
  5. 05Choose components you can replace. Clean interfaces buy you the right to be wrong once.
Tags
ArchitectureScalabilityDatabasesKafkaGo
Keep readingAll Posts