For a long time, this Azure Function felt like a success story.
It was small. It was event-driven. It scaled automatically.
On paper, it was exactly the kind of workload serverless is built for.
Eventually, we moved it out of serverless anyway.
Not because it was broken. Because it stopped being the right fit.
The Function That Kept Growing
The function started simple.
It processed inbound data, did some validation, and pushed results downstream. Execution time was short. Volume was low. Failures were rare.
Over time, a few things changed:
- traffic increased
- payloads got larger
- dependencies grew
- downstream systems became more sensitive to timing
None of these changes felt dramatic on their own.
But together, they pushed the function past the point where the abstraction was helping us.
The First Signs of Friction
The problems showed up operationally, not functionally.
We started seeing:
- inconsistent execution times
- retry storms when downstream systems slowed down
- harder-to-explain failures
- growing concern around timeouts and limits
Debugging became harder than it should have been.
The function still worked. It just felt fragile.
Observability Was the Turning Point
The real breaking point was visibility.
We needed to answer questions like:
- How long does this actually take under load?
- What happens when multiple executions overlap?
- Are we scaling or thrashing?
- Where is time being spent?
Azure Functions gave us some metrics, but not enough clarity to be confident.
At that point, the lack of control became a liability.
The Application Insights integration showed us invocations and durations. What it didn’t show was why performance varied so much between identical requests, or what was happening during cold starts versus warm executions.
We needed more than dashboards. We needed reproducibility.
The Cost of Invisible Constraints
Serverless hides constraints until you hit them.
Execution limits. Concurrency behavior. Networking quirks. Cold start patterns.
Once the function depended on predictable performance, those invisible constraints mattered.
We were designing around the platform instead of the workload.
That was the signal.
What We Moved It To
We did not jump straight to something complex.
We moved the workload to a more explicit execution model:
- predictable startup
- clearer scaling behavior
- tighter control over retries
- better observability
The code barely changed.
The mental model did.
We chose Azure App Service with a background worker pattern. Same language, same dependencies, same business logic. Just running in a process we controlled instead of one managed by the Functions runtime.
Scaling became explicit. Retries became intentional. Performance became predictable.
What Got Better Immediately
The difference was noticeable almost right away.
- execution times stabilized
- retries became intentional instead of automatic
- logs told a coherent story
- alerts became actionable
- incidents became easier to explain
Nothing about the business logic improved. The platform did.
What We Lost
We did lose some convenience.
- we had to think about scaling again
- we owned more configuration
- deployments were slightly heavier
That tradeoff was worth it.
We were already paying that cost mentally. We just were not acknowledging it.
This Was Not a Failure of Serverless
It is important to say this clearly.
Azure Functions did exactly what they are designed to do. They carried us through an early stage with minimal overhead.
Moving away was not a rejection of serverless. It was an acknowledgment that the workload had matured.
Serverless did its job.
We outgrew it. That’s success, not failure.
Final Thought
The hardest part of platform work is knowing when a tool has outlived its usefulness for a specific problem.
Not everything needs to be rewritten. Not everything needs to be modernized.
Sometimes the most responsible decision is to make execution boring and predictable again.
That is why we moved one function out of serverless.
Running Azure Functions in production? Sometimes the best migration is from serverless back to something simpler. Know when to make that call.