Back to News & Insights
DevOps October 9, 2026 · 7 min read

The same app dropped 51 requests on Kubernetes and none on DigitalOcean's App Platform

I deployed the worst version of my app, one that exits the instant it gets SIGTERM, to App Platform and redeployed it four times under load. 278,014 requests, none lost to a deploy. Then I found the one window where requests still fail, and the setting that closes it.

The same app dropped 51 requests on Kubernetes and none on DigitalOcean's App Platform

A few weeks ago I measured a Kubernetes rolling update dropping requests even with maxUnavailable: 0, the setting people choose precisely because they want zero downtime. The cure took three separate pieces: a preStop delay, graceful shutdown in the application, and closing keep-alive connections while draining. Leave any one of them out and connections died during every deploy.

That raised an obvious follow-up. Most people deploying a small web service never touch a Deployment manifest; they push to a platform and let it handle the rollout. So I took the worst version of the app from that experiment, the one that exits the instant it receives SIGTERM and does no draining of any kind, and deployed it to DigitalOcean's App Platform to see what the platform does on its behalf.

On Kubernetes that app lost 51 requests in a single rollout. On App Platform, across four redeploys and 278,014 requests, no request failed because of a deploy.

The server is the same Python file as before, with one change: every response now names the container that served it and a version number taken from an environment variable, so the load generator can see the exact moment traffic moves to the new version.

It runs on the smallest instance App Platform offers, one shared vCPU and 512 MB, as a single instance. The client is the same keep-alive load generator, twenty threads holding persistent connections and firing continuously, now over HTTPS to the app's public URL. Each test starts the load, waits thirty seconds, then changes the version number in the app spec, which triggers a redeploy, and keeps going until well after the new version is live.

Over the public internet, through a TLS terminator, nothing failed. Anything that fails during a deploy is the deploy's doing.

| | requests | failed | |---|---|---| | deploy 1 | 85,892 | 0 | | deploy 2 | 62,040 | 0 | | deploy 3 | 66,981 | 0 | | deploy 4 | 63,101 | 1 |

The single failure in the fourth run deserves a proper look rather than a footnote. It was a 502 that came back in three milliseconds, 167 seconds after that deploy had finished and the old container was long gone. The deploy didn't cause it. It is one request in about three hundred thousand going wrong at the edge while nothing was happening, and I am counting it in the table rather than leaving it out.

Each redeploy went live in under a minute, and the phase transitions are visible through doctl apps list-deployments as they happen:

The first question with a result this clean is what is absorbing the failures. The response headers answer part of it:

App Platform puts Cloudflare in front of every app, and my client's connections were to the Cloudflare edge in Sofia, not to the container. That alone removes the failure mode from the Kubernetes post, where the dying pod's keep-alive connections were severed mid-use, because here no client ever holds a connection to a pod that is going away.

The hostname is worth a second look too. web-96969cc79-cwvw8 has the shape of a Kubernetes pod name, a deployment, a replica-set hash and a random suffix. I can't see past the edge to confirm what runs underneath, but whatever it is, the platform is doing the work I had to do by hand last time.

The cutover itself was gentle. For 10.0 seconds both versions answered, interleaved almost exactly half and half, and then the old one stopped receiving new requests. Latency through the cutover was indistinguishable from either side of it, with a median of 42.9 ms against 44.3 ms beforehand.

Every request in those runs completed in about forty milliseconds, which leaves a question open. A platform that drains an instance before stopping it and a proxy that quietly retries a failed request would produce exactly the same zero. To tell them apart I needed requests that were still running when the old version was stopped.

So I added an endpoint that sleeps for twenty seconds before answering, and alongside the normal load started one of those every second, each on its own connection. Every response reports which version finished it.

The answer is that the platform drains, it does not retry. A request that reached the old version 4.1 seconds into the cutover still ran its full twenty seconds and was answered by the old version, and nothing anywhere in the run was silently re-executed on the new one.

The drain has a limit, though, and it is precise. After the old version received its last new request, it was kept alive for 15.0 seconds and then stopped. Two requests that had reached it in the final moments of the overlap were still sleeping at that point, and both failed at the same instant with a 504.

I ran the default case twice. Both times the window was 15.0 seconds, both times exactly two of 150 in-flight requests were cut off, and the moment the old container stopped differed between the runs by a fraction of a second relative to its last new request.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation