Most teams build web applications by allocating servers, managing deployments, and scaling based on traffic patterns. It is the dominant model. It is also a lot of work.
Azure Functions takes a different approach. You write a function. You connect it to a trigger (HTTP request, message queue, timer, blob change). Azure runs your function when the trigger fires. You pay only for the time the function executes. No servers to manage. No scaling to configure.
This sounds too good to be true, and there are real trade-offs. But for certain workloads, serverless with Azure Functions is genuinely simpler and cheaper than traditional approaches.
This article covers how Azure Functions work, when to use them, how to use them well, and the real costs and benefits.
What Is Serverless Computing?
Serverless computing is a model where:
- You write code that runs in response to events
- The cloud provider manages the infrastructure
- You pay only for execution time
- You do not think about servers, scaling, or deployment
The name is misleading. There are still servers. You just do not manage them.
Serverless vs. traditional compute
Traditional (VMs or containers):
- You provision servers
- You deploy code to those servers
- You monitor utilization and scale based on demand
- You pay for server time, whether they are being used or not
Serverless:
- You upload code
- Code runs when triggered
- Scaling is automatic
- You pay for execution time only
Trade-offs:
Serverless is simpler but less flexible. You cannot run background processes that do not respond to events. You cannot customize the runtime environment extensively. You are bound by limits (execution time, memory, concurrent executions).
Traditional is more complex but more flexible. You can run anything. You control the environment. You pay for idle time.
How Azure Functions Work
The execution model
Azure Functions runs your code in response to events:
Event happens (HTTP request, message in queue, timer fires)
↓
Azure detects the event
↓
Azure allocates a container with your runtime (Node.js, Python, C#, Java)
↓
Azure loads your function code
↓
Azure invokes your function with the event as input
↓
Your function executes and returns a result
↓
Azure sends the result back or takes action based on the result
↓
Container is kept warm (for a while) or discarded
Cold starts vs. warm starts
A cold start is when Azure has to allocate a new container and load your code. This takes time (500ms - 2s typically).
A warm start is when a container is already allocated and your code is loaded. This is fast (10-50ms).
For most use cases, cold starts are acceptable. For latency-sensitive applications (e.g., user-facing APIs), they can be a problem.
Options to mitigate cold starts:
1. Premium plan
Keep instances warm all the time. Costs more but eliminates cold starts.
2. Consumption plan with App Service Plan warmup
Reserve minimum instances that are always running.
3. Durable Functions
Use orchestration to manage state across multiple function executions, reducing need for cold restarts.
4. Accept cold starts
For most use cases, cold starts are rare enough that accepting them is fine.
Pricing model
Azure Functions uses a pay-per-execution model:
- First 1M executions per month: Free
- After that: $0.20 per million executions
- Plus: Memory-gb-seconds cost
For a function that executes 100K times per month for 1 second each, memory-gb-seconds is the main cost.
Example: Function that uses 512 MB for 1 second:
Cost = (512 / 1024) GB * 1 second * $0.000016 per GB-second
= 0.5 * 1 * $0.000016
= $0.000008 per execution
= $0.80 per million executions
Compare to traditional: A small App Service (B1 tier) costs ~$10-15/month minimum.
For low-traffic workloads, serverless is cheaper. For high-traffic workloads where you can stay on consumption plan, it is still often cheaper. For very high traffic or always-on workloads, traditional compute becomes cheaper.
Trigger Types and Patterns
Azure Functions can be triggered by many events.
HTTP trigger
Triggered by HTTP requests. Essentially a REST API endpoint.
[FunctionName("HelloWorld")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
HttpRequest req,
ILogger log)
{
return new OkObjectResult("Hello, World!");
}
Use case: Lightweight APIs, webhooks, scheduled reports.
Trade-off: Cold start affects user-facing latency. For high-traffic APIs, consider App Service instead.
Timer trigger
Triggered on a schedule (CRON-based).
[FunctionName("DailyCleanup")]
public static void Run(
[TimerTrigger("0 0 * * *")] TimerInfo myTimer)
{
// Runs daily at midnight
CleanupOldData();
}
Use case: Scheduled jobs, nightly reports, periodic health checks.
Trade-off: None really. This is a great use case for serverless.
Queue trigger
Triggered by messages in Azure Storage Queue or Service Bus.
[FunctionName("ProcessOrder")]
public static void Run(
[QueueTrigger("orders")] OrderMessage order,
ILogger log)
{
log.LogInformation($"Processing order {order.Id}");
// Process the order
}
Use case: Async job processing, background work, decoupling services.
Trade-off: Queue-based processing adds latency. For real-time work, use direct calls instead.
Blob trigger
Triggered when a blob is uploaded or modified.
[FunctionName("ProcessImage")]
public static void Run(
[BlobTrigger("images/{name}")] Stream image,
string name,
ILogger log)
{
log.LogInformation($"Processing image {name}");
// Resize, analyze, transform the image
}
Use case: Image processing, file transformations, data pipelines.
Trade-off: Latency between blob upload and function execution (typically seconds). Not suitable for real-time processing.
Event Grid trigger
Triggered by events from Event Grid (webhooks at scale).
[FunctionName("HandleStorageEvent")]
public static void Run(
[EventGridTrigger] EventGridEvent eventGridEvent,
ILogger log)
{
if (eventGridEvent.EventType == "Microsoft.Storage.BlobCreated")
{
// Handle blob creation
}
}
Use case: Complex event routing, cross-service orchestration.
Trade-off: Event Grid adds complexity. Good for decoupled event-driven systems.
Service Bus trigger
Triggered by messages in Service Bus (messaging at scale).
[FunctionName("HandleMessage")]
public static void Run(
[ServiceBusTrigger("my-topic", "my-subscription")] string message,
ILogger log)
{
log.LogInformation($"Received: {message}");
}
Use case: Large-scale message processing, pub/sub patterns.
Trade-off: Service Bus adds cost and operational complexity. Use when you need guaranteed message delivery and complex routing.
Durable Functions
Durable Functions extends Azure Functions to support complex, multi-step workflows.
Without Durable Functions
// User calls API
[FunctionName("PlaceOrder")]
public static async Task<IActionResult> PlaceOrder(Order order)
{
try
{
await ValidateOrder(order);
await ProcessPayment(order);
await ShipOrder(order);
return Ok("Order placed");
}
catch (Exception ex)
{
// If ProcessPayment fails after ValidateOrder succeeds,
// we need to manually rollback or track state.
// This gets complicated quickly.
return BadRequest(ex);
}
}
Challenges:
- If a step fails, how do you retry?
- How do you rollback if a later step fails?
- How do you resume if the process is interrupted?
- How do you track progress?
With Durable Functions
[FunctionName("PlaceOrderOrchestrator")]
public static async Task RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var order = context.GetInput<Order>();
try
{
await context.CallActivityAsync("ValidateOrder", order);
await context.CallActivityAsync("ProcessPayment", order);
await context.CallActivityAsync("ShipOrder", order);
}
catch (Exception)
{
// Automatically retried
// Or manually retry with await context.CallActivityAsync(...)
}
}
[FunctionName("ValidateOrder")]
public static async Task ValidateOrder([ActivityTrigger] Order order)
{
// Validate
}
[FunctionName("ProcessPayment")]
public static async Task ProcessPayment([ActivityTrigger] Order order)
{
// Process payment
}
[FunctionName("ShipOrder")]
public static async Task ShipOrder([ActivityTrigger] Order order)
{
// Ship order
}
Durable Functions handles:
- State tracking (which step are we on?)
- Retry logic (with backoff)
- Resumption (if interrupted, pick up where you left off)
- Compensation (rollback if something fails)
Use cases:
- Multi-step workflows (order processing, approval chains)
- Long-running operations (data pipelines, bulk operations)
- Complex orchestration (fan-out/fan-in patterns)
Human interaction patterns
Durable Functions can pause and wait for human input:
[FunctionName("ApprovalOrchestrator")]
public static async Task RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var request = context.GetInput<Request>();
// Wait for approval (with timeout)
var approvalTask = context.WaitForExternalEvent<bool>("Approved");
var timeoutTask = context.CreateTimer(
context.CurrentUtcDateTime.AddDays(1),
false);
var approved = await Task.WhenAny(approvalTask, timeoutTask);
if (approved)
{
await context.CallActivityAsync("ProcessRequest", request);
}
else
{
await context.CallActivityAsync("RejectRequest", request);
}
}
This is powerful for workflows that require human approval or decisions.
Best Practices for Azure Functions
1. Keep functions focused and small
A function should do one thing well.
Bad: A function that validates an order, processes payment, ships the order, sends confirmation email.
Good: Separate functions for each step, orchestrated with Durable Functions.
Why: Smaller functions are easier to test, debug, and scale independently.
2. Use dependency injection
Functions need dependencies (database connection, HTTP client, logger). Use dependency injection.
public class OrderProcessor
{
private readonly IOrderRepository _repository;
public OrderProcessor(IOrderRepository repository)
{
_repository = repository;
}
public async Task Process(Order order)
{
await _repository.SaveAsync(order);
}
}
// In Startup
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<OrderProcessor>();
}
3. Implement idempotency
Functions may be triggered multiple times for the same event (retry, duplicate message). Make them safe to run twice.
Bad:
[FunctionName("AddCredit")]
public static void AddCredit([QueueTrigger("credit-requests")] CreditRequest req)
{
database.AddCredit(req.UserId, req.Amount); // If called twice, credit added twice
}
Good:
[FunctionName("AddCredit")]
public static void AddCredit(
[QueueTrigger("credit-requests")] CreditRequest req,
[Blob("processed-credits/{id}")] CloudBlockBlob processedBlob)
{
if (processedBlob.Exists())
{
return; // Already processed
}
database.AddCredit(req.UserId, req.Amount);
processedBlob.UploadTextAsync("processed");
}
4. Handle errors and retries
Configure retry policies for triggers:
{
"version": "2.0",
"functionTimeout": "00:05:00",
"extensions": {
"queues": {
"maxDequeueCount": 3,
"visibilityTimeout": "00:00:30"
}
}
}
This retries failed queue messages 3 times before moving to dead-letter queue.
5. Monitor and log
Use Application Insights for monitoring:
[FunctionName("ProcessOrder")]
public static async Task Run(
[QueueTrigger("orders")] Order order,
ILogger log,
ExecutionContext context)
{
log.LogInformation($"Processing order {order.Id}");
try
{
// Process order
}
catch (Exception ex)
{
log.LogError($"Error processing order {order.Id}: {ex.Message}");
throw;
}
}
6. Secure your functions
Use authorization levels:
[HttpTrigger(AuthorizationLevel.Anonymous, ...)] // Public
[HttpTrigger(AuthorizationLevel.Function, ...)] // Requires function key
[HttpTrigger(AuthorizationLevel.Admin, ...)] // Requires master key
Store sensitive data in Key Vault:
public static IActionResult Run(
HttpRequest req,
[ServiceBus("...")] IAsyncCollector<string> queue,
ILogger log)
{
// ServiceBus connection string is managed automatically
}
7. Test functions locally
Use Azure Functions Core Tools to run functions locally:
func start
This lets you test triggers and bindings before deploying.
8. Use managed identities
Instead of connection strings in config, use managed identity:
var credential = new DefaultAzureCredential();
var client = new BlobContainerClient(
new Uri("https://myaccount.blob.core.windows.net/mycontainer"),
credential);
The function authenticates as itself. No secrets in config.
Where to Start with Azure Functions
If you are considering Azure Functions, here are good starting points:
1. Scheduled jobs
Timer-triggered functions are perfect for:
- Nightly data exports
- Periodic cleanup
- Health checks
- Report generation
These are almost always better than managing a separate job scheduler.
2. Webhook receivers
Use HTTP-triggered functions to receive webhooks from third-party services (GitHub, Stripe, Slack).
Much simpler than running a full web server.
3. Event-driven data processing
When something happens (file uploaded, message arrives), process it with a function.
Examples:
- Image resizing on upload
- Log analysis on new logs
- Data transformation on queue message
4. API endpoints for lightweight operations
Simple CRUD APIs can run on Functions instead of App Service.
Better economics if traffic is low or bursty.
5. Background job processing
Move async work to queue-triggered functions.
Example: User submits a report request via web UI. A queue message is added. A function processes the report asynchronously. Result is emailed to user.
Anti-patterns (When NOT to use Functions)
Long-running processes
If a function needs to run for > 10 minutes regularly, do not use Functions.
Cold starts, timeout limits, and execution costs make this inefficient.
Use: App Service, Container Instances, or a traditional job runner.
High-frequency APIs
If you have thousands of requests per second, Functions is probably not the right choice.
The overhead of cold starts and container management adds up.
Use: App Service, Container Instances, or a dedicated API platform.
Always-on processes
If you need a process running 24/7, Functions is inefficient.
Use: App Service with Always On enabled, or a traditional VM.
Complex, stateful logic
If you have complex interdependencies and state management, Functions becomes painful.
Use: App Service with a database, or a microservices framework.
Cost Management
Estimate your costs
Use the Azure pricing calculator:
Function executions: 10M/month
Memory: 256 MB
Duration: 1 second average
Cost = (256/1024) * 1 * 10M * $0.000016 + (10M/1M) * $0.20
= 2.5M * $0.000016 + 10 * $0.20
= $40 + $2
= $42/month
Compare to alternatives:
- App Service B1: $10-15/month (but always running, even idle)
- Container Instance: $0.0015/sec = ~$4.3K/month if always running
- VM: $15-50/month depending on size
Optimize spending
- Use lower memory if possible (128-256 MB often sufficient)
- Optimize code to reduce execution time
- Use Durable Functions to consolidate related operations
- Set function timeout appropriately (default 5 min, can increase)
Organizational Benefits
When you use Azure Functions effectively:
Reduced operational burden
No servers to patch, scale, or manage. Your ops team focuses on monitoring and alerting.
Faster time to market
No infrastructure to provision. Deploy a function and it is live.
Automatic scaling
Traffic spikes? Azure scales automatically. No capacity planning needed.
Cost efficiency
Pay for what you use. Idle time costs nothing.
Developer focus
Developers write business logic, not infrastructure code.
Common Pitfalls
Pitfall 1: Cold starts in user-facing APIs
Deploying a user-facing HTTP API to Functions without addressing cold starts causes latency issues.
Solution: Use Premium plan or App Service Plan with minimum instances.
Pitfall 2: No error handling
Functions fail silently if you do not implement error handling.
Solution: Use Application Insights, log everything, set up alerts.
Pitfall 3: Over-complex Durable Functions
Durable Functions is powerful but can become hard to debug.
Solution: Keep orchestrations simple. Use simple activity functions.
Pitfall 4: Not testing locally
Deploying to Azure without testing locally leads to surprises.
Solution: Use Azure Functions Core Tools and test locally first.
Pitfall 5: Ignoring cold starts
Cold starts matter for latency-sensitive workloads.
Solution: Understand your SLA. If latency < 1s is required, use Premium plan.
Conclusion
Azure Functions is a powerful tool for specific workloads. It is not appropriate for everything, but for scheduled jobs, event-driven processing, and lightweight APIs, it is genuinely superior to traditional approaches.
The key to using Functions successfully:
- Choose the right problems: Scheduled jobs, webhooks, async processing, lightweight APIs
- Keep functions small: One responsibility per function
- Handle errors: Retry, log, alert
- Test locally: Do not rely on cloud testing
- Monitor continuously: Know what is happening in production
- Use Durable Functions for complex workflows: It is worth the learning curve
When you get these right, Azure Functions reduces operational complexity, accelerates development, and often reduces costs. That is why it is popular, and why it will continue to be central to Azure deployments.