DEV Community

Cover image for Sentry's Span Hierarchy Exposed a Silent Retry in My 5-Agent Pipeline. One Agent Took 22.6s, the Others Took 5.
Sarvar Nadaf
Sarvar Nadaf

Posted on

Sentry's Span Hierarchy Exposed a Silent Retry in My 5-Agent Pipeline. One Agent Took 22.6s, the Others Took 5.

Summer Bug Smash: Clear the Lineup 🐛🛹

This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.


Project Overview

I built an AWS Security Posture Agent: five specialist AI agents that scan your AWS account for security misconfigurations, map findings to CIS benchmarks, score risk, and generate copy-paste fix commands.

The agents run sequentially on CrewAI with Amazon Bedrock Nova Pro as the LLM:

1. ResourceDiscovery    → inventories EC2, S3, Lambda, IAM, SGs, API GW, DynamoDB
2. SecurityScanner      → finds open ports, public buckets, admin roles, insecure configs
3. ComplianceChecker    → maps to CIS AWS Foundations Benchmark
4. RiskScorer           → severity × blast radius × exploitability
5. RemediationPlanner   → generates AWS CLI fix commands
Enter fullscreen mode Exit fullscreen mode

Each agent has custom boto3 tools that make real AWS API calls against a live account with 90 IAM roles, 14 S3 buckets, 9 security groups, and 7 Lambda functions. Not test data. Real findings.


GitHub logo simplynadaf / aws-security-posture-agent

Multi-agent AI system that scans AWS accounts for security misconfigurations using CrewAI + Amazon Bedrock, instrumented with Sentry AI Agent Monitoring. 5 specialist agents discover resources, analyze security, map to CIS benchmarks, score risk, and generate fix commands.

AWS Security Posture Agent

Five AI agents scan your AWS account. Find misconfigurations. Score risk. Get fix commands.

Python CrewAI Sentry Bedrock License

Quick Start | Architecture | Screenshots | Performance Fix


The Problem

Most AWS accounts accumulate security debt silently. Open SSH ports from testing. S3 buckets without encryption. IAM roles with full admin access that nobody remembers creating. Manual audits miss things. AWS Config rules cost money. SecurityHub is noisy.

This agent scans your account in 60 seconds, finds real issues, maps them to CIS benchmarks, scores risk, and gives you the exact AWS CLI command to fix each one.

What It Finds

On a real AWS account (90 IAM roles, 14 S3 buckets, 9 security groups, 7 Lambda functions):

97 security findings
├── CRITICAL: open SSH/RDP from 0.0.0.0/0, IAM users without MFA
├── HIGH:     admin roles, unencrypted EBS, missing public access blocks
├── MEDIUM:   deprecated Lambda runtimes, missing versioning, stale SGs
└──

Demo

Here's the full scan running against my AWS account. The scan portion is sped up 4x, results walkthrough is at normal speed:


Bug Fix or Performance Improvement

My first instinct was to blame Bedrock latency. Every time something is slow you blame the LLM, right?

The SecurityScanner agent was taking 22.6 seconds while every other agent averaged 5-10 seconds. Without visibility into what was happening inside each agent's execution, I would have added time.time() calls and guessed.

Sentry's trace waterfall told a different story. The real problem wasn't Python execution time or network latency. It was the LLM getting a context payload it couldn't process cleanly on the first attempt.

The root cause: my IAMAnalyzer tool was fetching all 90 IAM roles from the account (59 after filtering service-linked roles), serializing them into a 27KB JSON blob, and handing that entire payload to the LLM as tool output. The context window got overwhelmed. CrewAI's internal retry logic kicked in, burning tokens on a second attempt with even more context.

One tool. Wrong default. The entire pipeline suffered.


Code

PR with the fix:

fix: paginate IAM analysis and add token budget guard #1

What

The IAMAnalyzer tool was fetching every single role in the account (90 of them, 59 after filtering service-linked ones) and dumping the full details into a single JSON blob. That blob hit 26,980 characters. The LLM choked on it, CrewAI retried the task, and the SecurityScanner agent ended up taking 22.6s while every other agent finished in 5-10s.

How I found it

Added Sentry spans to each agent and tool. The trace waterfall made it obvious: SecurityScanner was twice as wide as everything else. Drilled into the tool spans and saw result_length_chars: 26980 on iam_analyzer vs ~4000 on the other tools.

7x output disproportion. That was the problem.

What I changed

Three things in iam_analyzer.py:

  1. Paginate and sort roles by RoleLastUsed date, only analyze the top 20 most active ones. The rest are stale roles nobody has touched in months.
  2. Skip 31 service-linked roles upfront (you cannot modify them anyway, auditing them is noise).
  3. Token budget guard at the end: if the JSON output exceeds 4000 chars, trim the role_summary down to just names and policy lists.

Numbers

  • Tool output: 26,980 chars down to 15,532 (42% smaller)
  • SecurityScanner: 22.6s down to 17.8s (21% faster, no more LLM retry)
  • API calls to IAM: 59 down to 20
  • Total pipeline: 62s down to 57.7s
  • Findings: still 27. No coverage loss because the top-20 most active roles contain all the problematic ones (AdminRole, Bedrock-Lambda-Role, etc are all heavily used).

The core change lives in src/security_posture/tools/iam_analyzer.py. Here's the before and after:

Before (the bug):

# Fetches ALL roles without pagination limit
roles = iam.list_roles(MaxItems=100)
role_details = []

for role in roles["Roles"]:
    role_name = role["RoleName"]
    if role.get("Path", "").startswith("/aws-service-role/"):
        continue
    # Analyzes every single role...
    attached = iam.list_attached_role_policies(RoleName=role_name)
    # ...builds massive JSON output
Enter fullscreen mode Exit fullscreen mode

This produces 26,980 characters of JSON for an account with 90 roles. The LLM chokes on it.

After (the fix):

# FIX: Paginate and sort by relevance
all_roles = []
paginator = iam.get_paginator("list_roles")
for page in paginator.paginate():
    all_roles.extend(page["Roles"])

# Filter service-linked roles (31 roles, can't modify anyway)
auditable_roles = [
    r for r in all_roles
    if not r.get("Path", "").startswith("/aws-service-role/")
]

# Sort by last used date (most active first)
auditable_roles.sort(key=_last_used_sort_key, reverse=True)

# Take only top 20 roles
roles_to_analyze = auditable_roles[:max_roles]
Enter fullscreen mode Exit fullscreen mode

Plus a token budget guard at the end:

# Token budget guard: truncate if output exceeds threshold
if len(output) > 4000:
    result["role_summary"] = [
        {"role_name": r["role_name"], "policies": r.get("attached_policies", [])}
        for r in role_details
    ]
    result["note"] = "Role details truncated to stay within token budget"
    output = json.dumps(result, indent=2, default=str)
Enter fullscreen mode Exit fullscreen mode

Three changes. Pagination, relevance sorting, and a safety valve. The SecurityScanner stopped retrying.


My Improvements

Results first:

Metric Before After Improvement
IAM tool output 26,980 chars 15,532 chars 42% smaller
SecurityScanner time 22.6s 17.8s 21% faster
IAM API calls 59 20 66% fewer
Total pipeline 62.0s 57.7s 7% faster
Security findings 97 97 No coverage loss

The 21% improvement on the SecurityScanner came from the LLM completing analysis in a single pass instead of retrying.

How I found it:

The fix itself is simple. The interesting part is how Sentry pointed me straight to it.

Without the trace waterfall, all I would have seen is "pipeline takes 62 seconds." The trace said otherwise:

  1. Wrap each agent execution in a gen_ai.invoke_agent span
  2. Wrap each tool call in a gen_ai.execute_tool span
  3. Run the pipeline and look at the trace waterfall
  4. The SecurityScanner span was visually obvious: twice the width of everything else
  5. Inside it, the iam_analyzer tool span showed a result_length_chars of 26,980
  6. Compare to security_group_analyzer at 4,200 chars and s3_config_checker at 3,800 chars

The disproportion was the clue. If the tool output is 7x larger than its siblings, reduce it.


Best Use of Sentry

I used Sentry's AI Agent Monitoring to instrument a multi-agent CrewAI pipeline from scratch. This isn't a web app or API. It's five autonomous AI agents making LLM calls and executing custom tools. Standard APM wouldn't help here.


What I instrumented

Every pipeline run creates a Sentry transaction with this span hierarchy:

Transaction: "Security Posture Scan" (57s)
├── gen_ai.invoke_agent: ResourceDiscovery
│   └── gen_ai.execute_tool: aws_resource_scanner
├── gen_ai.invoke_agent: SecurityScanner
│   ├── gen_ai.execute_tool: security_group_analyzer
│   ├── gen_ai.execute_tool: s3_config_checker
│   ├── gen_ai.execute_tool: iam_analyzer
│   ├── gen_ai.execute_tool: ec2_security_checker
│   └── gen_ai.execute_tool: lambda_security_checker
├── gen_ai.invoke_agent: ComplianceChecker
├── gen_ai.invoke_agent: RiskScorer
└── gen_ai.invoke_agent: RemediationPlanner
Enter fullscreen mode Exit fullscreen mode

The instrumentation code

For each agent (in main.py):

with start_span(
    op="gen_ai.invoke_agent",
    name=f"invoke_agent {agent_name}",
) as agent_span:
    agent_span.set_data("gen_ai.operation.name", "invoke_agent")
    agent_span.set_data("gen_ai.agent.name", agent_name)
    agent_span.set_data("gen_ai.request.model", "amazon.nova-pro-v1:0")
    agent_span.set_data("gen_ai.pipeline.name", "security-posture-scan")

    task_output = task_obj.execute_sync(
        agent=task_obj.agent,
        context=context,
        tools=task_obj.agent.tools,
    )

    agent_span.set_data("duration_seconds", round(elapsed, 2))
    agent_span.set_data("output_length_chars", len(str(task_output)))
Enter fullscreen mode Exit fullscreen mode

For each tool (decorator in monitoring.py):

def trace_tool(tool_name: str):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            with start_span(
                op="gen_ai.execute_tool",
                name=f"execute_tool {tool_name}",
            ) as span:
                span.set_data("gen_ai.tool.name", tool_name)
                result = func(*args, **kwargs)
                span.set_data("result_length_chars", len(result))
                try:
                    data = json.loads(result)
                    if "findings_count" in data:
                        span.set_data("findings_count", data["findings_count"])
                except (json.JSONDecodeError, KeyError):
                    pass
                return result
        return wrapper
    return decorator
Enter fullscreen mode Exit fullscreen mode

What Sentry revealed

The trace waterfall made the bottleneck visually obvious. The SecurityScanner span was nearly twice the width of any other agent, 22.6 seconds while others averaged 5-10s.

Inside it, the iam_analyzer tool span showed result_length_chars: 26980 while s3_config_checker showed 3,800 and security_group_analyzer showed 4,200. The granularity goes down to individual boto3 calls. Every GetPublicAccessBlock, ListRoles, and ListAttachedRolePolicies shows up as its own span.

Before the fix (SecurityScanner dominates the trace at 22.6s):

Sentry trace waterfall showing SecurityScanner agent dominating the pipeline at 22.6s

Zoomed span detail showing iam_analyzer tool with result_length_chars 26980

After the fix (all agents proportional, no single bottleneck):

After fix - all agent spans proportional, SecurityScanner no longer the bottleneck

Sentry transaction overview showing clean 57s pipeline with no retries

The iam_analyzer output dropped from 26,980 chars to 15,532, and the SecurityScanner stopped triggering retry logic.


Sentry features used

Feature How I Used It
Distributed Tracing Full pipeline trace from start to final report
AI Agent Monitoring gen_ai.invoke_agent spans for all 5 agents
Tool Execution Tracing gen_ai.execute_tool spans for all 5 boto3 tools
Custom Span Data Token counts, output sizes, duration, finding counts
Error Monitoring Exception capture with sentry_sdk.capture_exception()
Breadcrumbs Agent completion events via task callbacks
Transaction Metadata Model name, pipeline config, agent count

Why this matters for AI agent observability

Standard logging tells you an agent "finished." It doesn't tell you which tool inside which agent returned a 27KB payload that triggered a retry you never asked for.

With five agents each making their own LLM calls and tool executions, you need span-level visibility. Sentry's gen_ai.invoke_agent and gen_ai.execute_tool conventions gave me exactly that. I could see the problem in the trace waterfall before I even looked at the code.

That's the difference between "add some logging" and actual AI observability.


If you're running multi-agent pipelines, what's your observability setup? Curious if anyone else has hit similar tool-output-size problems with CrewAI or LangGraph.

The agent found 97 real security findings in my AWS account. The fix is in production.

Top comments (23)

Collapse
 
mudassirworks profile image
Mudassir Khan

the 27KB blob → context overflow → silent retry is exactly the class of bug trace waterfalls catch and time.time() never would. we hit the same wall with a RAG retrieval tool returning full document bodies. agent 'worked', just with the wrong answer, for two days tbh.

fix that stuck: every tool with variable size output now runs a token budget ceiling check before handing off to the LLM. for list style tools, paginate AND summarize over the ceiling rather than truncate. truncating cuts the tail which is where the interesting fields live.

curious whether you added spans at the boto3 call level or just the CrewAI agent boundary? agent level is easier to wire but boto3 is where you actually catch the tail latency culprits.

Collapse
 
sarvar_04 profile image
Sarvar Nadaf

yeah same thing happened to me. truncating was my first attempt and it cut off the RoleLastUsed dates which were the whole point stale roles are the security risk not the active ones. switched to sorting by last used and only returning top 20. that fixed it without losing any findings.

on spans just crewai level right now. agent start/stop and tool start/stop no boto3 level was enough for this bug because the retry showed as a second span with bigger input so easy to spot in the waterfall. but yeah for pinpointing which api call is actually slow inside the tool i'd need to go deeper haven't needed to yet.

your RAG thing sounds worse tbh. mine was just slow yours was giving wrong answers and nobody noticed for two days. at least slow makes you look into it wrong just sits there looking fine. paginate and summarize over truncate stealing that. truncating drops the end of the list which is where the stale unused roles live. exactly the stuff a security scan needs most.

Collapse
 
mudassirworks profile image
Mudassir Khan

the sort before capping is the right move — truncating an unordered list means you have no idea what you're losing. same with RAG: we rank by relevance score first then cap, not the reverse.

wrong vs slow is real. wrong answers that look plausible sit in prod for weeks. that's what pushed us toward evals over eyeballing. do you run any systematic checks on the security scan outputs or is it mostly manual review?

Collapse
 
jn_141414 profile image
JN

I remember reading your previous post on this topic, and it was already insightful. This one takes it to another level. It really shows how Sentry can help developers with observability and debugging in a practical way, rather than just explaining the features. Nicely written thanks for sharing this!

Collapse
 
sarvar_04 profile image
Sarvar Nadaf

Thank you so much! Means lot to me 🙏🏻

Collapse
 
jn_141414 profile image
JN

Yep ur welcome

Collapse
 
steven_r_404 profile image
Steven Ray

hit this same issue with langgraph last month. kept thinking it was the model being slow until i actually timed individual tool calls. turned out one retrieval tool was returning the entire document instead of just the relevant chunks. how are you deciding the 4000 char limit specifically is that based on nova pro's context window or just trial and error?

Collapse
 
sarvar_04 profile image
Sarvar Nadaf

mostly trial and error honestly 4000 felt like the point where the agent stopped retrying. i didn't do any scientific testing around it just ran it a few times watched the sentry spans and picked a number that killed the retry behavior. probably should formalize that but it works for now.

Collapse
 
steven_r_404 profile image
Steven Ray

Perfect! Thanks

Thread Thread
 
sarvar_04 profile image
Sarvar Nadaf

Your Welcome!

Collapse
 
komo profile image
Reid Marlow

The 4000-char guard is the right kind of ugly fix. I’d make it less magic by logging output_bytes, retry_count, and finish_reason per task, then pick the cutoff from the first knee in that chart. The failure mode here is not “slow model”; it’s unbounded tool output pretending to be normal context.

Collapse
 
sarvar_04 profile image
Sarvar Nadaf

yeah the 4000 number is basically what made it stop retrying no science behind it. logging output_bytes and retry_count per task is the right move i just havent wired that up yet right now i only have result_length_chars on the span which got me to the problem but doesnt help me pick a better threshold dynamically. the knee in the chart approach makes sense especially if i end up scanning accounts with way more roles where 4000 might be too aggressive. good call on finish_reason too hadnt thought about surfacing that from crewais internals once again thanks for great approch i will explore it for sure.

Collapse
 
pratikponde profile image
Pratik Ponde

This is a solid example of why end-to-end tracing is becoming essential for AI agent pipelines. The silent retry could have easily gone unnoticed without span-level visibility. Nice breakdown of the investigation process and the fix.

Collapse
 
icophy profile image
Cophy Origin

The "27KB JSON blob → LLM context overwhelm → silent retry" pattern is something I've run into in a different form building multi-step agent pipelines. The failure is nearly invisible without tracing because the output looks correct — just slower. Sentry's span hierarchy making that 7x output disproportion visible is exactly the kind of observability primitive that changes how you think about debugging agents vs. services.

The token budget guard at the output layer is a clean fix. I'd be curious whether you considered applying it upstream (filtering before analysis) vs. downstream (trimming before returning) — in your case the "top 20 by RoleLastUsed" heuristic is smart because it preserves coverage while cutting noise, but in cases where recency ≠ risk that tradeoff gets trickier.

One thing I keep thinking about for pipelines like this: the context explosion problem is really a data-to-reasoning ratio problem. Each tool's job is to compress real-world state into something an LLM can reason about cleanly. When a tool returns raw API dumps, you're asking the LLM to do the compression work — which it will retry its way through. Moving compression into the tools themselves (as you did here) is the right architecture.

Collapse
 
mia_keller_ffd2584c046ecb profile image
Mia Keller

Fantastic write-up! Silent retries in multi-agent frameworks are such a sneaky bottleneck—especially when the tool output context keeps bloating on consecutive attempts.

Wrapping the execution in custom Sentry spans for both invoke_agent and execute_tool is a super clean pattern. The "compression/budgeting in the tool itself" approach seems like the most practical way forward for agentic workflows right now. Thanks for sharing the detailed before-and-after numbers!

Collapse
 
sarvar_04 profile image
Sarvar Nadaf

thanks mia! sentry's span hierarchy was the real game changer here without seeing invoke_agent and execute_tool as separate spans i would've just blamed bedrock being slow. instead the trace showed a duplicate span with higher input tokens which is the silent retry in action. curious are you using sentry for your agentic workflows? if so do you track precompression vs post compression output size as span attributes? i started with result_length_chars but thinking about adding tokens_saved so i can alert when a tool is trimming too aggressively. how are you handling that balance?

Collapse
 
sarvar_04 profile image
Sarvar Nadaf

The 27KB output thing caught me off guard. I assumed Bedrock was just slow because... it's an LLM, they're all slow sometimes. Turns out CrewAI was retrying the entire task silently because the context got too fat. No error no warning just doubled execution time. Anyone else running into context size issues with multi-agent frameworks? I'm curious if langgraph handles tool output overflow differently or if it's the same problem everywhere.

Collapse
 
kartik-nvjk profile image
Kartik N V J K

The silent retry is the part that would have cost me hours, since CrewAI swallowed the failure and re-ran with even more context, so the retry was worse than the original call. I hit something similar with an uncapped tool returning paginated AWS resources, and a hard character ceiling on the tool output before it returns was the cleanest fix. Did the 22.6s agent surface any warning at all, or was span timing the only signal you had?

Collapse
 
sarvar_04 profile image
Sarvar Nadaf

Honestly no warning at all. crewai just retries silently and the second attempt actually gets more context stuffed in because it includes the failed attempt too so it gets worse not better span timing was literally the only signal i had. without the tool level spans i would have just thought bedrock was being slow that day and moved on. out of curiosity did you put the char ceiling inside the tool itself or do you have something wrapping the output before it goes back to the agent?

Collapse
 
salman_khan_c31307505285e profile image
Salmankhan

Wow, Sarvar I think this invention gonna take DEVOPS Engineer to another level.

Collapse
 
sarvar_04 profile image
Sarvar Nadaf

Yes for sure!