Airflow vs n8n vs Make for API orchestration
What You’ll Need
To follow this guide and test these platforms yourself, you’ll need:
- n8n Cloud or self-hosted n8n instance
- Hetzner VPS or Contabo VPS for self-hosting options
- DigitalOcean as an alternative cloud hosting provider
- A code editor (VS Code recommended)
- Basic API knowledge and a free API key (we’ll use a public API for testing)
Table of Contents
- Why Orchestration Matters
- Airflow Deep Dive
- n8n: The Visual Alternative
- Make: The No-Code Powerhouse
- Direct Comparison: Which Should You Choose?
- Getting Started
Why Orchestration Matters
API orchestration is the backbone of modern automation. You’re not just calling one endpoint—you’re chaining multiple services together, handling failures, transforming data, and doing it all reliably at scale.
I’ve managed this across three different platforms, and the differences are significant. Some teams spend weeks setting up what should take days. Others pick a tool that scales perfectly for their current needs but hits a wall at 10x growth.
This isn’t theoretical. I’ve deployed Airflow on Kubernetes, built production n8n workflows handling 50K daily requests, and integrated Make workflows into client dashboards. Each tool has genuine strengths and real limitations.
Airflow Deep Dive
Apache Airflow is the heavyweight champion. It’s what Netflix, Spotify, and Uber run under the hood. If you’re orchestrating hundreds of jobs across distributed systems, Airflow is battle-tested.
What Airflow Does Well:
Airflow excels at complex DAGs (Directed Acyclic Graphs) with sophisticated retry logic, branching, and conditional execution. You define workflows as Python code, which means unlimited flexibility. The scheduler is rock-solid. If you need enterprise-grade SLA monitoring and audit trails, Airflow has been hardened over a decade.
The Reality Check:
Setup is brutal. You’re deploying a database, Redis instance, Celery workers, and a web UI. A basic Airflow cluster on Hetzner VPS will run you about €50/month minimum, plus DevOps overhead. A single developer can’t spin this up in an afternoon.
Here’s what a simple Airflow DAG looks like:
from datetime import datetime, timedelta
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.operators.http import SimpleHttpOperator
from airflow.utils.decorators import apply_defaults
import requests
import json
default_args = {
'owner': 'data-team',
'retries': 3,
'retry_delay': timedelta(minutes=5),
'start_date': datetime(2024, 1, 1),
'email_on_failure': True,
'email': ['alerts@company.com'],
}
dag = DAG(
'api_orchestration_pipeline',
default_args=default_args,
description='Orchestrate multiple API calls with error handling',
schedule_interval='0 */6 * * *',
catchup=False,
)
def fetch_user_data(**context):
response = requests.get('https://jsonplaceholder.typicode.com/users/1')
response.raise_for_status()
user_data = response.json()
context['task_instance'].xcom_push(key='user_id', value=user_data['id'])
context['task_instance'].xcom_push(key='user_email', value=user_data['email'])
return user_data
def fetch_user_posts(**context):
user_id = context['task_instance'].xcom_pull(task_ids='fetch_user_data', key='user_id')
response = requests.get(f'https://jsonplaceholder.typicode.com/posts?userId={user_id}')
response.raise_for_status()
posts = response.json()
context['task_instance'].xcom_push(key='post_count', value=len(posts))
return posts
def process_and_store(**context):
user_email = context['task_instance'].xcom_pull(task_ids='fetch_user_data', key='user_email')
post_count = context['task_instance'].xcom_pull(task_ids='fetch_user_posts', key='post_count')
result = {
'email': user_email,
'post_count': post_count,
'processed_at': datetime.now().isoformat(),
'status': 'success'
}
print(f"Processing complete: {json.dumps(result, indent=2)}")
return result
def handle_failure(**context):
task_instance = context['task_instance']
exception = context.get('exception')
print(f"Task failed: {task_instance.task_id}")
print(f"Exception: {str(exception)}")
task_fetch_user = PythonOperator(
task_id='fetch_user_data',
python_callable=fetch_user_data,
dag=dag,
)
task_fetch_posts = PythonOperator(
task_id='fetch_user_posts',
python_callable=fetch_user_posts,
dag=dag,
on_failure_callback=handle_failure,
)
task_process = PythonOperator(
task_id='process_and_store',
python_callable=process_and_store,
dag=dag,
)
task_fetch_user >> task_fetch_posts >> task_process
This is straightforward, but notice you’re managing dependencies, XCom push/pull for inter-task communication, and error callbacks manually. Scale this to 50 tasks, and your DAG file becomes a maintenance nightmare. When you’re running Airflow on DigitalOcean , you also need monitoring infrastructure, log aggregation, and on-call rotations.
n8n: The Visual Alternative
n8n is what I reach for most often. It’s a node-based orchestration platform that bridges the gap between “I need code” and “I can’t manage infrastructure.”
Why n8n Wins:
With n8n Cloud , you get a fully managed, scalable platform without DevOps. The visual workflow builder lets you iterate fast. You can chain 50 API calls visually, and the execution time is transparent. Self-hosting on a Contabo VPS costs €4/month. The conditional logic, error handling, and data transformation all happen in the UI.
I built a workflow last month that fetches data from five different APIs, deduplicates, transforms, and writes to PostgreSQL—all visual, zero Python. It handles 5,000 runs daily on a single VPS.
Here’s what that workflow looks like in JSON (n8n’s export format):
{
"name": "Multi-API Data Pipeline",
"nodes": [
{
"parameters": {
"authentication": "none",
"url": "https://jsonplaceholder.typicode.com/users",
"method": "GET"
},
"name": "Fetch Users",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [300, 200]
},
{
"parameters": {
"method": "POST",
"url": "https://jsonplaceholder.typicode.com/posts",
"headers": {
"Content-Type": "application/json"
},
"body": "{\n \"userId\": {{ $json.id }},\n \"title\": \"Auto-generated post\",\n \"body\": \"Created via n8n orchestration\"\n}"
},
"name": "Create Post for Each User",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [600, 200]
},
{
"parameters": {
"functionCode": "const results = [];\nfor (const item of items) {\n const user = item.json;\n results.push({\n id: user.id,\n name: user.name,\n email: user.email,\n company: user.company.name,\n processed_at: new Date().toISOString()\n });\n}\nreturn results;"
},
"name": "Transform User Data",
"type": "n8n-nodes-base.functionItem",
"typeVersion": 1,
"position": [900, 200]
},
{
"parameters": {
"operation": "executeQuery",
"database": "automation_db",
"query": "INSERT INTO users (id, name, email, company, processed_at) VALUES ({{ $json.id }}, '{{ $json.name }}', '{{ $json.email }}', '{{ $json.company }}', '{{ $json.processed_at }}') ON CONFLICT(id) DO UPDATE SET email = EXCLUDED.email, company = EXCLUDED.company, processed_at = EXCLUDED.processed_at;"
},
"name": "Save to PostgreSQL",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2,
"position": [1200, 200]
},
{
"parameters": {
"conditions": {
"nodeVersion": 2.2,
"conditions": [
{
"value1": "{{ $json.processed_at }}",
"operator": "exists"
}
]
}
},
"name": "Check if Processed",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [1500, 200]
},
{
"parameters": {
"channel": "#automation-logs",
"text": "Pipeline execution successful. Processed {{ $input.all().length }} records at {{ now().format('YYYY-MM-DD HH:mm:ss') }}"
},
"name": "Slack Notification",
"type": "n8n-nodes-base.slack",
"typeVersion": 2,
"position": [1800, 200]
}
],
"connections": {
"Fetch Users": {
"main": [
[
{
"node": "Transform User Data",
"type": "main",
"index": 0
}
]
]
},
"Transform User Data": {
"main": [
[
{
"node": "Save to PostgreSQL",
"type": "main",
"index": 0
}
]
]
},
"Save to PostgreSQL": {
"main": [
[
{
"node": "Check if Processed",
"type": "main",
"index": 0
}
]
]
},
"Check if Processed": {
"main": [
[
{
"node": "Slack Notification",
"type": "main",
"index": 0
}
],
[]
]
}
}
}
This workflow fetches users, loops through each (n8n handles that automatically), transforms the data with a JavaScript function, writes to PostgreSQL, validates completion, and sends a Slack notification.
Want to automate this yourself?
Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.