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:

  1. Trigger fires with input data
  2. Workflow executes in memory
  3. Actions process data sequentially or in parallel
  4. On completion, all state is discarded
  5. No history or checkpoint is saved

Create Stateless Workflow

Azure Portal

  1. Open Logic App (Standard) in Azure Portal
  2. Go to "Workflows"
  3. Click "Add" → "Create"
  4. Provide workflow name
  5. Toggle "Stateful" to OFF (creates stateless)
  6. 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

FeatureStatefulStatelessImpact
Run History✅ Full❌ NoneCannot audit past runs
Checkpointing✅ After each action❌ NoneNo retry on failure
Persistence✅ Azure Storage❌ In-memoryStorage cost
Orchestration✅ Scopes, loops, conditionsLimitedCannot pause
Correlation✅ Tracking IDs❌ NoneHard to debug
LatencyHigher (I/O)Lower (memory)Performance
ThroughputLowerHigherScalability

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

SettingDescriptionRecommended
workflowOperationsConcurrencyMax parallel actions10-50
concurrency (trigger)Max parallel runs20-100
maximumActionCountMax actions per run60-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

LimitationImpactWorkaround
No run historyCannot review past runsLog to external service
No checkpointFailure loses progressAdd retry at caller level
No pause/resumeCannot stop and continueUse Azure Functions
No scopesCannot group actionsFlatten workflow
No complex loopsLarge iterations problematicChunk into batches
Limited debuggingLess visibilityAdd 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

  1. Analyze workflow — Identify state dependencies
  2. Remove limitations — Refactor scopes, complex loops
  3. Add logging — External logging for observability
  4. Test thoroughly — Verify behavior in staging
  5. 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

PracticeDescription
Use for high-volumeBest for 100+ runs/minute
Add external loggingMaintain observability
Configure concurrencyTune for your load
Keep workflows simpleAvoid complex nesting
Handle errors externallyDon't rely on checkpoint retry
Test with loadVerify 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