Scale application capacity
Vertical scaling for application scalability is underrated because it's boring. Doubling the instance size is a config change and a restart that buys real headroom. AWS will rent you a single instance with 1,920 vCPUs and 32 TiB of memory, so "we outgrew one machine" is a claim that needs evidence behind it.
Scale up until one of two things is true: the cost curve turns against you, or you need redundancy that a single instance can't give. Redundancy is the better reason. One instance is one deploy away from downtime regardless of how big it is.
Once you go horizontal, put a load balancer in front. NGINX if you want to run it yourself, AWS Elastic Load Balancing or Azure Load Balancer if you'd rather not. Health checks matter more than the choice of balancer, because a balancer routing traffic to a half-dead instance is worse than no balancer.
For automatic capacity, Kubernetes Horizontal Pod Autoscaler or a managed autoscaling group both work, but tune the timing. The HPA default scale-down stabilization window is 300 seconds while scale-up has no stabilization at all, which is the right asymmetry for spiky traffic and the wrong one for a batch workload that thrashes. Scale on the metric that reflects your constraint, so queue depth for workers and request concurrency for web tiers, rather than CPU by default.
Relieve the data tier
Application instances are cheap to add. The database is not, which is why the data tier is where a scalable architecture either holds or falls over.
Read replicas come first, and only when reads are what's constraining the primary. Notion routes 90% of read traffic to replicas to keep pressure off the primary. The cost is replication lag, so every read path has to declare whether it tolerates data a few hundred milliseconds old. A user reading their own just-submitted comment does not.
Sharding is last, and only when one database provably cannot meet measured demand. Notion sharded into 480 logical shards across 32 physical databases, keyed on workspace ID, after transaction ID wraparound threatened to stop writes entirely. That's what a forcing function looks like. They also picked 480 because it divides cleanly by many numbers, which let them later grow to 96 physical hosts without redistributing everything.
Some workloads resist distribution, and it's worth knowing which before you commit:
-
Cross-entity transactions, because a two-phase commit across shards trades a database guarantee for application code you now maintain
-
Joins across the shard key, which turn into scatter-gather queries bounded by your slowest shard
-
Strong consistency requirements, since replicas and shards both introduce windows where reads disagree
-
Uneven access, where one tenant is 40% of your traffic and lands on one shard anyway
If your access pattern has a natural partition key like tenant or workspace, sharding works well for application scalability. If it doesn't, you're building a distributed join engine, and that project is much larger than it looks.
Two scaling outcomes
Timing is the whole variable. The same scalable architecture that saves one team destroys another team's velocity, and the difference is whether a measured constraint existed when they built it.
Complexity at 500 users
Segment split its monolith into microservices roughly a year after launch, then spent three years adding to them. Alexandra Noonan, the engineer who led the move back, described where it ended up: "if they are implemented incorrectly or used as a Band-Aid without addressing some of the root flaws in your system, you'll find yourself no longer to make any new product development because you'll be drowning in the complexity."
A team at 500 daily active users repeating that pattern loses in predictable ways. Every feature crosses service boundaries, so a one-day change becomes three coordinated deploys. A trace goes missing between two services and debugging turns into archaeology. On-call now covers eleven deployables, each with its own pipeline and dashboards, while the actual request volume would fit comfortably on one modest instance. The velocity cost is immediate and the capacity benefit is zero, because there was no application scalability problem to solve.
Focus past 100k users
The opposite path is unglamorous and works. Find the top three consumers of wall-clock time in application performance and fix them. Indexing the columns your hot queries filter on. Batching the N+1 that your list endpoints generate. Caching the reads that repeat thousands of times an hour.
Systems well past 100,000 users run this way today. Stack Overflow serves more than 6,000 requests per second from a monolith on nine web servers and a single primary SQL Server with a hot standby. Shopify's Ruby monolith peaked at more than 117 million requests per minute on app servers during Black Friday-Cyber Monday.
Neither example proves your system will scale the same way. Their access patterns and data models are specific to them. What they do prove is that "we've outgrown this architecture" is a claim requiring measurement, because the ceiling of a well-tuned single deployable sits far higher than most roadmaps for scalable architecture assume.
Improve application scalability sequentially
Do this in order, and stop as soon as the numbers stop justifying the next step. Measure and name the constraint. Fix the query layer. Cache the repeated reads. Scale vertically while it's still cheap. Move state out of process. Add instances behind a load balancer. Only then distribute data or extract a service.
Pollume builds and hosts web and full-stack applications for startups and growing companies, which means we spend a lot of time in codebases where cost and latency climbed before anyone profiled anything. If you want a bottleneck assessment and a delivery plan that improves application scalability without a rewrite you don't need, get in touch.