A growing API and a working CI/CD pipeline can make Kubernetes look like the next sensible purchase. For a 15–30-person engineering organisation, I would usually decline it. The hard part is rarely getting the first deployment running; it is maintaining the relationship between application releases, scaling rules, permissions, and operational alerts after the engineers who set them up move on.
Autoscaling turns releases into a standing compatibility job
The prompt What fails first when your simple API hits 10x traffic directs attention to saturation. That is useful, but a traffic spike does not, by itself, establish a need for a new orchestrator. If PostgreSQL connections are exhausted, adding application replicas can make the outage worse because each replica opens another pool. If the bottleneck is a slow downstream call, more workers can increase pressure on that dependency for the same reason.
Kubernetes adds another question to every release: does this application version still behave correctly under the cluster’s scheduling and shutdown rules? A readiness probe that returns HTTP 200 before a service can handle real requests may send traffic to an unready process. A termination grace period shorter than a long-running request may interrupt work during a rollout. Neither setting is a one-time infrastructure decision, because changes to startup, connection pooling, and background jobs can invalidate it.
That ongoing coupling is the maintenance cost worth pricing. Someone must own the Kubernetes Horizontal Pod Autoscaler configuration, PodDisruptionBudgets, resource requests, ingress rules, and the application’s response to SIGTERM. Someone must also determine whether a bad release calls for a rollback, a scale-up, or a database intervention. Prometheus and Grafana can show CPU use and request latency, but dashboards cannot make that judgment because the same symptom can have different causes.
I would not adopt Kubernetes to address the first large traffic increase, because it expands the set of things the team must diagnose before establishing which resource is actually constrained. I would first compare p95 latency, error rate, PostgreSQL connection count, and queue depth during a representative load test. OpenTelemetry traces can help identify whether time is spent in the API, a database query, or a downstream request. The outcome might justify more capacity; it might instead justify a smaller connection pool or a query fix.
A successful pipeline does not pay its own upkeep
The exercise Build a CI CD pipeline in 2 days then decide buy vs build can test how quickly a team gets code into production. It cannot price the quarterly work of keeping that path safe, because the first successful run does not encounter every future runner update, credential rotation, image change, or rollback.
Consider a GitHub Actions workflow that builds a Docker image, pushes it to Amazon ECR, and deploys through Amazon ECS. Its initial success is only the beginning of ownership. The team still needs to pin actions such as actions/checkout@v4 and docker/build-push-action@v6, review updates, maintain the build image, and verify that the deployment job cannot use broader AWS IAM permissions than it needs. Using GitHub’s OIDC integration avoids storing a long-lived AWS access key, but the IAM trust policy still needs review when repositories or workflow names change.
A cluster adds a second compatibility track to that pipeline. The image must work with the deployment manifest; the manifest must satisfy admission rules; and changes to ingress or autoscaling must be released without surprising existing services. Helm can package those changes, but a chart version still needs an owner who understands its defaults. Terraform can record infrastructure changes, but its plan still needs a reviewer able to distinguish an expected replacement from an accidental one. These tools reduce repeatable manual work; they do not eliminate decisions.
Give that work an explicit budget. Set aside an initial allowance of 3 engineer-hours a week for pipeline and deployment upkeep; this is a planning value to revise against timesheets or issue history, not a claim about what teams generally spend. Count failed builds caused by tooling, dependency updates, permission changes, and deployment-specific incidents separately from application defects. After a quarter, the recorded time will give the tech lead a better basis for a platform decision than setup time alone.
Fargate wins until scheduling becomes a product requirement
For a small team running a conventional HTTP API, I would compare Amazon ECS on Fargate with Kubernetes on Amazon EKS rather than compare an imagined “simple cloud” with an imagined “flexible platform.” Both options require container images, IAM decisions, networking, observability, and deployment procedures. Their difference is how much orchestration policy the team chooses to own.
ECS on Fargate wins when the service needs a few deployable containers, predictable networking through an Application Load Balancer, and scaling based on a metric the team can explain. It costs money for provisioned task resources, and AWS-specific task definitions make a future move to another scheduler real work. EKS wins when multiple services genuinely need Kubernetes scheduling features, shared controllers, or deployment conventions already maintained by people who know them. It costs additional cluster administration and upgrade coordination, even when AWS manages the control plane, because application teams still own workloads, add-ons, and their configurations.
Do not decide from a CPU chart alone. An API that spends most of its time waiting on PostgreSQL may need database work rather than additional tasks on either platform. Likewise, scaling on request count can mislead if one endpoint takes substantially more work than another. AWS documents the Application Load Balancer’s default idle timeout as 60 seconds; that vendor-published default is worth checking against legitimate long requests before treating a timeout as a capacity problem.
Require a concrete reason to pay the EKS maintenance premium. “We may have more services later” is insufficient because it does not identify a capability missing from ECS today. A stronger reason would be an existing Kubernetes platform owner serving several teams, with documented upgrade windows and a support path. Without that owner, standardising on EKS can shift work onto whichever application engineer happens to understand the last failed rollout.
Maintenance needs a release test as well as a load test
A load test should answer a narrow question before it becomes a purchasing argument. The following k6 script sends traffic to a representative staging endpoint and fails its run if error rate or latency crosses provisional thresholds. Run it with k6 run -e BASE_URL=https://staging.example.com probe.js, replacing the address and route with a safe staging target.
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
steady: { executor: 'constant-arrival-rate',
rate: 30, timeUnit: '1s', duration: '2m', preAllocatedVUs: 40 }
},
thresholds: { http_req_failed: ['rate<0.01'], http_req_duration: ['p(95)<500'] }
};
export default function () {
const res = http.get(`${__ENV.BASE_URL}/api/items?limit=20`);
check(res, { 'status is 200': r => r.status === 200 });
}
The 30 requests per second rate is a trial setting to calibrate against actual demand, not an estimate of the organisation’s peak. The 500 ms p95 limit is likewise a provisional service target; change it if user-facing evidence supports a different limit. A passing result says little about production capacity if staging uses a smaller database, warmer caches, or different downstream services. Record those differences alongside the result rather than filing the graph as proof that scaling is solved.
Then test the part load tools miss: release the API while the test runs. Watch whether requests fail during replacement, whether old tasks finish work, whether PostgreSQL connections surge, and whether an operator can identify the active version quickly. Rehearse a rollback using the same GitHub Actions or deployment path used for ordinary releases. If rollback requires editing a manifest by hand or finding an engineer who knows a hidden console setting, that is a maintenance defect even when steady-state latency looks good.
Keep a small decision record for each change to scaling or deployment behaviour: the symptom, the chosen setting, the person responsible for reviewing it, and the condition that would make the team reverse it. This is more useful than an evergreen architecture diagram because an on-call engineer needs to know why a limit exists before changing it under pressure.
Start by counting the work nobody has claimed
At the next planning meeting, list every production deployment and scaling setting that has no named reviewer. Assign owners to the rollback path, IAM trust policy, load-test target, and database connection limit first, because each can turn an ordinary release into an incident. Time the upkeep for one quarter. Buy more orchestration only when that record shows a missing capability worth maintaining.
