Last week I fixed three bugs in my AI publishing pipeline. They looked unrelated. They weren’t.

The root cause was a single architectural decision I’d made without thinking too hard about it: I was caching health status data in Redis with a 12-hour TTL. That one call — reasonable on the surface, wrong in practice — propagated invisibly across three different layers of the system and produced three distinct failure modes, each one masking the next.

Here’s the full chain.


Background: What the Pipeline Does

FABRIC/SOCIAL is the content automation platform I’ve built for ry-ops.dev. At the center of it is Aiana — an AI publishing strategist embedded in the UI. When I ask Aiana to create a campaign, she responds with a structured JSON object that includes a full blog post in markdown, scheduling metadata, and platform targeting. The backend parses that JSON, commits the blog post to GitHub, queues the LinkedIn carousel, and kicks off the YouTube Short render.

The whole thing is held together by intent detection: Aiana classifies my message, returns the appropriate JSON shape, and the backend handler runs. When it works, it’s seamless. When it breaks, it breaks in layers.


Bug 1: The Token Ceiling

The create_campaign intent embeds a full blog post inside the JSON response. A typical blog post in my system runs 800–1,200 words of markdown. The JSON wrapper adds metadata, frontmatter, and scheduling fields. The total payload is large.

I had set maxTokens: 2048 on the Claude API call that handles intent responses. That limit made sense when Aiana was returning short chat replies and post variants. It made no sense once create_campaign started embedding full markdown documents.

What happened: Claude hit the token ceiling mid-object. The JSON closed nowhere — no closing brace, no closing quote on the markdown string, just a hard cutoff. JSON.parse() threw. The catch block, not knowing what else to do, returned the raw truncated blob as a chat message.

I saw a wall of JSON in the UI. No error message. No indication the campaign hadn’t been created. Just a truncated markdown blob staring back at me.

The fix:

  • Raised maxTokens to 8192 for all intent responses
  • Added a regex salvage pass in the catch block — if the raw response looks like a JSON object with a truncated string, attempt to close it and re-parse before giving up
  • Added explicit logging when salvage fires so I know it happened

The salvage pass is a safety net, not a solution. The real fix is the token limit. But the logging matters: a silent salvage is another form of lying.


Bug 2: The Silent Queue

Five blog posts were stuck in validating stage.

validating in my pipeline means: saved to Redis, not yet dispatched to GitHub Actions. It’s a transient state — posts should move through it in seconds. If a post sits in validating for more than a few minutes, something upstream broke.

What broke was Bug 1. The JSON truncation meant the create_campaign intent handler never ran to completion. The blog post metadata got written to Redis — far enough in the flow to persist — but the GitHub Actions dispatch call never fired. The posts existed. The pipeline didn’t know they needed to go anywhere.

From the outside, the queue looked thin. From the inside, five posts were sitting in a state the system wasn’t designed to recover from automatically.

The fix:

  • Re-dispatched all 5 posts manually via the blog-publish workflow
  • Added proactive validating detection to Aiana’s context — if a post has been in validating for more than 10 minutes, Aiana surfaces it in the daily brief and offers to re-dispatch
  • Added the detection note to the system prompt so Aiana flags it in conversation, not just in the briefing

The proactive detection is the important part. A post stuck in validating is invisible unless you’re looking for it. The system now looks for it.


Bug 3: The Confident Lie

This is the one that actually bothered me.

After fixing Bugs 1 and 2 — after re-dispatching the five posts, after confirming they were queued, after verifying the pipeline was healthy — I asked Aiana for a status update.

She reported: “Queue critically thin — 1 post queued.”

Five posts were scheduled. The queue was fine. Aiana was wrong.

The cause: Aiana’s daily briefing was cached in Redis with a 12-hour TTL. The cache had been written before the re-dispatch. It reflected the broken state of the pipeline, not the fixed one. The cache had no way to know the world had changed. So it confidently reported a problem that no longer existed.

This is the failure mode that caching health status data always produces, eventually: a stale “your system is broken” message delivered with the same confidence as a live reading.

The fix:

  • Removed the briefing cache entirely
  • Health status is now always computed live from Redis
  • The 12-hour TTL is gone

The performance argument for caching the briefing was weak to begin with. The briefing aggregates data from a handful of Redis keys — queue depth, campaign counts, alert state. That’s milliseconds of compute, not a database join across millions of rows. The cache was premature optimization at the cost of correctness.


The Root Cause Is One Decision

Three bugs. One root cause.

I cached health status data because it felt like the right thing to do. Caching is a default instinct in backend work — if something is read frequently and computed from multiple sources, cache it. Reduce load. Improve response time.

But health status is different. Health status answers the question: what is the current state of the system? The moment that answer is stale, it’s worse than useless. It’s actively misleading. And when the thing reading that status is an AI agent making recommendations — telling you what to write next, flagging what’s broken, scheduling your content — a stale answer doesn’t just confuse you. It erodes trust in the entire system.

I caught the confident lie because I knew the queue had been fixed. If I hadn’t been in the middle of debugging, I might have believed Aiana. I might have started adding posts to fill a gap that didn’t exist. I might have double-published.

The rule I’m taking away from this: never cache health status data. Not with a short TTL. Not with cache invalidation on write. Just don’t cache it. Compute it live, every time. The cost of being wrong is higher than the cost of the extra compute.


What “Validating” Detection Actually Looks Like

For anyone building something similar, here’s the shape of the proactive detection I added. Aiana’s system prompt now includes:

RECENT BLOG POSTS:
  NOTE: "validating" = post saved to Redis but NOT dispatched to GitHub Actions (stuck).
  If you see a "validating" stage post, flag it to Ryan and offer to re-dispatch
  via the blog-publish workflow.

This means every time Aiana gets a context injection — which happens on every message — she sees the current blog post stages. If anything is stuck in validating, she surfaces it immediately, in conversation, without waiting for me to notice.

It’s a simple pattern: surface operational state in the agent’s context, not just in a dashboard. The agent is already reading the context. Make the context tell the truth.


The Cascading Failure Pattern

What made this incident interesting — and worth writing about — is the cascade shape.

Bug 1 caused Bug 2. The token truncation broke the handler, which left posts in validating. Bug 2 wasn’t detectable without looking at Redis directly, because the surface-level symptom (thin queue) was ambiguous. Was the queue thin because nothing had been created, or because something was stuck? The briefing didn’t say.

Bug 3 was independent in cause but dependent in timing. The cache happened to be written during the broken state, so it preserved the broken state’s picture of the world. If the cache had expired naturally before I checked, I would have seen the correct queue depth and never noticed the caching problem at all.

Cascading failures in content pipelines are annoying. Cascading failures in AI-assisted workflows are worse, because the AI is supposed to be the observability layer. When the observability layer lies — even because of stale cache, not hallucination — you lose the one thing that makes the AI useful: the ability to trust what it tells you.


What I’d Do Differently

  1. Set token limits per intent, not globally. create_campaign needs 8192. A chat reply needs 512. Treat them differently from the start.

  2. Make stuck states visible in the agent’s context immediately. Don’t wait for a dashboard. Put it in the prompt.

  3. Never cache health status. Compute it live. Always. The performance cost is negligible. The trust cost of a stale reading is not.

  4. Log salvage operations explicitly. If your error handling is silently recovering from a parse failure, you need to know it happened. Silent recovery is a bug waiting to surface.

  5. Test the failure chain, not just the happy path. I had unit tests for the JSON parser. I didn’t have an integration test that fed a token-truncated response through the full intent handler and checked the pipeline state afterward. That test would have caught Bugs 1 and 2 together.


The Pipeline Is Clean Now

All five stuck posts are dispatched. The queue is healthy. The briefing is live. The token limit is 8192. The salvage pass is logging.

And the 12-hour cache is gone.

If Aiana tells me the queue is thin, I can trust it. That’s worth more than any performance optimization I was getting from the cache.


FABRIC/SOCIAL is the content automation platform behind ry-ops.dev. Aiana is the AI publishing strategist embedded in it. Both are built and operated by Ryan Dahlberg.

Frequently Asked Questions

What happens when Claude hits the maxTokens limit inside a JSON response?

When Claude reaches the maxTokens ceiling mid-generation, it stops writing immediately with no graceful closure — leaving JSON objects without closing braces or unterminated strings. Calling JSON.parse() on this truncated output throws a SyntaxError, and if the catch block is not designed to handle it specifically, the raw malformed blob can be passed downstream as if it were a valid response.

Why is caching health status data a bad practice in AI pipelines?

Health status data reflects real-time system state, and caching it with a long TTL means the pipeline operates on stale information about which services, models, or integrations are actually available. This can cause the pipeline to route requests to degraded or misconfigured components without triggering alerts, making bugs harder to detect and attribute correctly.

How do you debug cascading bugs in a multi-layer AI content pipeline?

Cascading bugs in AI pipelines are best debugged by tracing each failure back to its earliest upstream decision rather than fixing symptoms in isolation. In practice, this means logging the raw model output before any parsing step, validating token limits against actual payload sizes, and ensuring health checks reflect live state rather than cached approximations.

What is the right maxTokens value for a Claude API call that returns full blog posts in JSON?

For a create_campaign intent that embeds 800–1,200 words of markdown inside a JSON wrapper with metadata and frontmatter, a maxTokens value of 2048 is insufficient. A minimum of 4096 tokens is recommended, with 8192 preferred to provide headroom for longer posts and avoid mid-object truncation.