Azure Logic Apps — Stateless Workflows for High Throughput
No Run History, Lower Latency, Standard Tier Only
Introduction
Azure Logic Apps Standard tier introduces two workflow types: Stateful and Stateless. Stateless workflows are designed for scenarios where you need maximum throughput and minimum latency without the overhead of persisting execution history.
This guide covers:
- Stateless architecture — How it differs from stateful
- Use cases — When to use each type
- Implementation — Creating and configuring stateless workflows
- Performance tuning — Optimizing for high throughput
- Migration strategies — Converting stateful to stateless
Understanding Stateless Workflows
Architecture Comparison
┌───────────────────────────────────────────────────────────────────┐
│ STATEFUL vs STATELESS │
├───────────────────────────────────────────────────────────────────┤
│ │
│ STATEFUL: STATELESS: │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Trigger Event │ │ Trigger Event │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Save State │ │ Process In- │ │
│ │ to Storage │ │ Memory Only │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Execute Action │ │ Execute Action │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ ▼ │ │
│ ┌──────────────────┐ │ │
│ │ Update History │ │ │
│ │ + Checkpoint │ │ │
│ └────────┬─────────┘ │ │
│ │ ▼ │
│ │ ┌──────────────────┐ │
│ └──────────────────────▶│ Complete │ │
│ └──────────────────┘ │
│ │
│ Storage: Azure Storage Storage: None (in-memory) │
│ Latency: Higher Latency: Lower │
│ Durability: Full Durability: Limited │
│ │
└───────────────────────────────────────────────────────────────────┘
How Stateless Works
When a stateless workflow runs:
- Trigger fires with input data
- Workflow executes in memory
- Actions process data sequentially or in parallel
- On completion, all state is discarded
- No history or checkpoint is saved
Create Stateless Workflow
Azure Portal
- Open Logic App (Standard) in Azure Portal
- Go to "Workflows"
- Click "Add" → "Create"
- Provide workflow name
- Toggle "Stateful" to OFF (creates stateless)
- Configure trigger and actions
JSON Definition
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2019-05-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"kind": "Stateless",
"triggers": {
"When_a_message_is_received": {
"type": "ServiceProvider",
"inputs": {
"serviceProviderConfiguration": {
"connectionName": "servicebus",
"operationName": "ReceiveMessage"
},
"parameters": {
"queueName": "orders"
}
}
}
},
"actions": {
"Process_Order": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://api.example.com/process",
"body": "@triggerBody()"
}
},
"Send_Response": {
"type": "ServiceBus",
"inputs": {
"host": {
"connection": {
"name": "servicebus"
}
},
"method": "send",
"body": "@outputs('Process_Order')",
"path": "/messages",
"queueName": "processed-orders"
}
}
}
},
"parameters": {}
}
CLI Creation
# Create stateless workflow
az logic workflow create \
--resource-group my-rg \
--logic-app-name my-logic-app \
--name my-stateless-workflow \
--definition @./stateless-workflow.json \
--location eastus
Stateful vs Stateless Deep Dive
Feature Comparison
| Feature | Stateful | Stateless | Impact |
|---|---|---|---|
| Run History | ✅ Full | ❌ None | Cannot audit past runs |
| Checkpointing | ✅ After each action | ❌ None | No retry on failure |
| Persistence | ✅ Azure Storage | ❌ In-memory | Storage cost |
| Orchestration | ✅ Scopes, loops, conditions | Limited | Cannot pause |
| Correlation | ✅ Tracking IDs | ❌ None | Hard to debug |
| Latency | Higher (I/O) | Lower (memory) | Performance |
| Throughput | Lower | Higher | Scalability |
When to Use Stateful
- Human workflows — Need approval, pause, resume
- Long-running processes — Days/weeks duration
- Complex error handling — Retry with state recovery
- Audit requirements — Full execution history
- Complex branching — Nested scopes and conditions
When to Use Stateless
- High-volume processing — 1000s of events/minute
- Fire-and-forget — No need to track completion
- Real-time transformations — Low latency required
- Simple workflows — Sequential actions, no loops
- Cost optimization — Reduce storage costs
Use Cases and Examples
High-Volume Event Processing
{
"definition": {
"kind": "Stateless",
"triggers": {
"When_events_arrive": {
"type": "EventHub",
"inputs": {
"eventHubName": "high-volume-events",
"consumerGroup": "$Default"
}
}
},
"actions": {
"Transform_Event": {
"type": "JavaScript",
"inputs": {
"code": "var event = triggerBody(); return { id: event.id, value: event.value * 2, timestamp: new Date().toISOString() };"
}
},
"Publish_Transformed": {
"type": "EventHub",
"inputs": {
"eventHubName": "transformed-events",
"content": "@outputs('Transform_Event')"
}
}
}
}
}
Real-Time Notification
{
"definition": {
"kind": "Stateless",
"triggers": {
"HTTP_Request": {
"type": "Request",
"kind": "Http",
"inputs": {}
}
},
"actions": {
"Send_Email": {
"type": "Office365",
"inputs": {
"body": {
"To": "@triggerBody()['email']",
"Subject": "Notification",
"Body": "@triggerBody()['message']"
},
"host": {
"connection": {
"name": "office365"
}
},
"operation": "SendEmail"
}
},
"Log_Notification": {
"type": "Log",
"inputs": {
"message": "Notification sent to @triggerBody()['email']"
}
}
}
}
}
Data Transformation Pipeline
{
"definition": {
"kind": "Stateless",
"triggers": {
"Blob_Created": {
"type": "Blob",
"inputs": {
"path": "input/{filename}"
}
}
},
"actions": {
"Read_Content": {
"type": "Blob",
"inputs": {
"path": "input/@triggerBody()?['filename']"
}
},
"Transform_Data": {
"type": "JavaScript",
"inputs": {
"code": "var data = JSON.parse(inputs('Read_Content')); return data.map(x => ({...x, processed: true}));"
}
},
"Write_Output": {
"type": "Blob",
"inputs": {
"path": "output/@triggerBody()['filename']",
"body": "@outputs('Transform_Data')"
}
}
}
}
}
Performance Configuration
Configure Concurrency
{
"configuration": {
"executionMode": "Stateless",
"runtimeConfiguration": {
"workflowOperationsConcurrency": 20,
"maximumActionCount": 60
},
"triggers": {
"runtimeConfiguration": {
"concurrency": {
"run": 50
}
}
}
}
}
Settings Explained
| Setting | Description | Recommended |
|---|---|---|
workflowOperationsConcurrency | Max parallel actions | 10-50 |
concurrency (trigger) | Max parallel runs | 20-100 |
maximumActionCount | Max actions per run | 60-100 |
Azure Portal Configuration
Logic App → Workflow → Settings → Runtime
├── Execution mode: Stateless
├── Concurrency control: Enabled
│ └── Degree of parallelism: 50
└── Operations concurrency: 20
Parallel Execution
ForEach in Stateless Mode
{
"actions": {
"Get_Batch": {
"type": "ServiceBus",
"inputs": {
"queueName": "order-batch",
"maxMessageCount": 100
}
},
"Process_All": {
"type": "ForEach",
"foreach": "@body('Get_Batch')",
"operationOptions": "Sequential", // or "Parallel"
"actions": {
"Process_Item": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://api.example.com/process",
"body": "@item()"
}
}
}
}
}
}
Batch Processing with SplitOn
{
"triggers": {
"When_batch_arrives": {
"type": "ServiceBus",
"inputs": {
"queueName": "orders",
"maxMessageCount": 50
}
}
},
"actions": {
"Process_All": {
"type": "ForEach",
"foreach": "@triggerBody()",
"actions": {
"Process_One": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://api.example.com/process",
"body": "@item()"
}
}
}
}
}
}
Limitations and Workarounds
What You Cannot Do
| Limitation | Impact | Workaround |
|---|---|---|
| No run history | Cannot review past runs | Log to external service |
| No checkpoint | Failure loses progress | Add retry at caller level |
| No pause/resume | Cannot stop and continue | Use Azure Functions |
| No scopes | Cannot group actions | Flatten workflow |
| No complex loops | Large iterations problematic | Chunk into batches |
| Limited debugging | Less visibility | Add logging actions |
Workaround Patterns
Pattern 1: External Logging
{
"actions": {
"Log_Start": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://logs.example.com/api/track",
"body": {
"workflow": "order-processing",
"event": "started",
"data": "@triggerBody()"
}
}
},
"Process": { ... },
"Log_Complete": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://logs.example.com/api/track",
"body": {
"workflow": "order-processing",
"event": "completed",
"result": "success"
}
}
}
}
}
Pattern 2: External State
{
"actions": {
"Set_Initial_Status": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://state.example.com/status",
"body": {
"correlationId": "@guid()",
"status": "started"
}
}
},
"Process": { ... },
"Update_Status": {
"type": "Http",
"inputs": {
"method": "PUT",
"uri": "https://state.example.com/status/@outputs('Set_Initial_Status').correlationId",
"body": {
"status": "completed"
}
}
}
}
}
Migration: Stateful to Stateless
When to Migrate
- Throughput requirements exceed stateful capacity
- Latency needs are critical
- Storage costs are too high
- Workflow doesn't need history
Migration Steps
- Analyze workflow — Identify state dependencies
- Remove limitations — Refactor scopes, complex loops
- Add logging — External logging for observability
- Test thoroughly — Verify behavior in staging
- Deploy — Switch to stateless type
Example Refactoring
// BEFORE: Stateful with scope
{
"kind": "Stateful",
"actions": {
"Scope_Processing": {
"type": "Scope",
"actions": {
"Step1": { ... },
"Step2": { ... }
}
}
}
}
// AFTER: Stateless - flatten
{
"kind": "Stateless",
"actions": {
"Step1": { ... },
"Step2": { ... }
}
}
Best Practices
| Practice | Description |
|---|---|
| Use for high-volume | Best for 100+ runs/minute |
| Add external logging | Maintain observability |
| Configure concurrency | Tune for your load |
| Keep workflows simple | Avoid complex nesting |
| Handle errors externally | Don't rely on checkpoint retry |
| Test with load | Verify performance under load |
Monitoring Stateless Workflows
# Get run statistics
az monitor app-insights query \
--app my-logic-app-insights \
--query "customEvents | where name == 'WorkflowStarted' | summarize count()"
# Check for failures
az monitor app-insights query \
--app my-logic-app-insights \
--query "exceptions | where operation_Name contains 'Stateless'"
Azure Integration Hub - Advanced Level