DEV Community

dodou
dodou

Posted on

SERP API in Event-Driven Architecture: Queues + Async Processing

Why SERP API Suits Event-Driven

SERP API calls have characteristics that make event-driven a natural fit:

  • 1-3s latency (sync waiting)
  • 0.3% failure rate (auto-refund)
  • Bursty traffic (5x peak on SEO tool launch)
  • Results consumable async (user doesn't need SERP immediately)

These make SERP API ideal for event-driven architecture—async the calls, decouple producer and consumer.

3 Typical Architectures

Architecture 1: Simple Queue

[Web] → [API Gateway] → [Queue: serp:jobs]
                              ↓
                       [Worker Pool] → [SerpBase API]
                              ↓
                       [Result Store: Redis/DB]
                              ↓
                       [Web polls result]
Enter fullscreen mode Exit fullscreen mode

Best for: 100-500 QPS, latency not critical.

Architecture 2: EventBridge + Lambda

[Web] → [API Gateway] → [EventBridge]
                                ↓
                         [Lambda: Worker]
                                ↓
                         [SerpBase API]
                                ↓
                         [DynamoDB: Result]
                                ↓
                         [WebSocket: push to client]
Enter fullscreen mode Exit fullscreen mode

Best for: AWS full stack, serverless, auto-scaling.

Architecture 3: Kafka + Flink

[Producer App] → [Kafka: serp-events]
                     ↓
              [Flink: real-time processing]
                     ↓
              [SerpBase API](batch)
                     ↓
              [Kafka: serp-results]
                     ↓
              [Consumers: dashboard / alert / archive]
Enter fullscreen mode Exit fullscreen mode

Best for: Big data streaming, need real-time SERP analysis.

4 Key Design Points

1. Idempotency

# Job must be idempotent (same job ID, same result)
def process_serp_job(job):
    if redis.exists(f"serp:done:{job['id']}"):
        return
    result = fetch_serp(job["query"])
    redis.setex(f"serp:done:{job['id']}", 86400, "1")
    save_result(job["id"], result)
Enter fullscreen mode Exit fullscreen mode

2. Dead Letter Queue

def process_serp_job(job):
    try:
        result = fetch_serp(job["query"])
        save_result(job["id"], result)
    except Exception as e:
        job["retry_count"] = job.get("retry_count", 0) + 1
        if job["retry_count"] >= 3:
            kafka.send("serp:dlq", job)
        else:
            kafka.send("serp:jobs", job)
Enter fullscreen mode Exit fullscreen mode

3. Backpressure

# Queue length > 1000 → degrade to sync
if redis.llen("serp:jobs") > 1000:
    return fetch_serp_directly(query)
Enter fullscreen mode Exit fullscreen mode

4. Dead Letter Monitoring

# DLQ length > 10 → alert
if redis.llen("serp:dlq") > 10:
    alert("SERP DLQ backlog growing!")
Enter fullscreen mode Exit fullscreen mode

Real Example: SEO Monitor System (Architecture 1)

# Producer: Web submits monitor task
@app.post("/api/monitor")
def submit_monitor(keyword: str, client_id: str):
    job = {"keyword": keyword, "client_id": client_id, "ts": time.time()}
    redis.lpush("serp:jobs", json.dumps(job))
    return {"status": "queued", "id": job_id}

# Consumer: Worker pool
def worker():
    while True:
        job = redis.brpop("serp:jobs", timeout=5)
        if not job:
            continue
        process_serp_job(json.loads(job[1]))

def process_serp_job(job):
    job_id = hash(f"{job['client_id']}:{job['keyword']}:{job['ts'] // 86400}")
    if redis.exists(f"serp:done:{job_id}"):
        return

    result = fetch_serp(job["keyword"])
    redis.hset(f"serp:results:{job['client_id']}", job["keyword"], json.dumps(result))
    redis.setex(f"serp:done:{job_id}", 86400, "1")
Enter fullscreen mode Exit fullscreen mode

Real Example: AI Agent Async SERP (Architecture 2)

# AWS Lambda processes SERP
def lambda_handler(event, context):
    query = event["query"]
    user_id = event["user_id"]

    data = fetch_serp(query)

    table.put_item(Item={
        "user_id": user_id,
        "query": query,
        "result": json.dumps(data),
        "ts": int(time.time()),
    })

    apigateway.post_to_connection(
        ConnectionId=event["connection_id"],
        Data=json.dumps({"query": query, "result": data})
    )
Enter fullscreen mode Exit fullscreen mode

Real Example: Real-time SERP Stream (Architecture 3)

# Kafka consume SERP events
def consume_serp_events():
    for message in kafka.consumer("serp-events"):
        event = json.loads(message.value)
        result = fetch_serp(event["query"])
        kafka.producer("serp-results", json.dumps({
            "event_id": event["id"],
            "query": event["query"],
            "result": result,
            "ts": time.time(),
        }))
Enter fullscreen mode Exit fullscreen mode

4 Key Benefits

Benefit Sync Event-Driven
Bursty traffic stuck queue absorbs
Failure recovery user sees error auto retry + fallback
Resource use 24/7 loaded elastic scaling
Observability hard message tracing complete

Monitoring Alerts

Metric Threshold Alert
Queue length > 1000 Slack (backpressure)
DLQ length > 10 Slack + oncall
Worker utilization > 80% Slack (scale workers)
Processing latency P50 > 30s Slack (slow)
Auto-refund rate > 5% Slack (vendor issue)

5 Best Practices for SERP API Integration

  1. Must be idempotent: same job retried = same result
  2. Must have DLQ: 3 failures → DLQ for human review
  3. Must have backpressure: queue too long → degrade to sync
  4. Must monitor: queue length + DLQ + worker utilization
  5. Must rate limit: Worker pool ≤ SERP API QPS limit

100 free searches: serpbase.dev signup, small-scale event-driven test.

Top comments (0)