2026-08-04 23:44:16
Go 1.24 introduces a transformative redesign of its map implementation, shifting from the traditional bucket + overflow-chain model to a Swiss Table-inspired design. This transition addresses long-standing inefficiencies in cache locality and memory usage, which have historically limited Go’s scalability in memory-intensive applications. The old model, while functional, suffered from pointer-chasing—a mechanical process where the CPU must follow multiple memory references to resolve collisions, leading to cache misses and degraded performance. In contrast, the Swiss Table design leverages control-byte metadata and `h2` filtering to optimize lookup behavior, reducing memory overhead and improving cache coherence. This redesign is not just a theoretical improvement; it’s a practical response to the growing demands of high-performance computing and cloud-native development, where efficient data structures are critical.
The stakes are high: without this redesign, Go maps would continue to face scalability bottlenecks, particularly in scenarios with high contention or large datasets. The Swiss Table approach, however, introduces a mechanism for minimizing memory fragmentation and optimizing hash distribution, allowing for higher practical load factors. This is achieved by storing metadata in a compact, contiguous format, which reduces the need for pointer-chasing and ensures that memory access patterns align with CPU cache lines. The result is a more predictable and efficient memory layout, which directly translates to faster lookups and insertions.
However, this transition is not without trade-offs. Go’s unique constraints—such as iteration semantics, garbage collector (GC) integration, and incremental growth behavior—required a tailored implementation of the Swiss Table design. For instance, maintaining iteration semantics while resizing the map dynamically involves a delicate balance between memory efficiency and performance. Similarly, GC integration demands careful memory management to avoid unnecessary overhead. Benchmarks reveal large microbench wins but smaller full-application gains, indicating that real-world benefits depend heavily on workload characteristics. Edge cases, such as cold-cache scenarios and delete/clear-heavy paths, remain areas of ongoing optimization, highlighting the complexity of balancing memory efficiency with performance.
In summary, the Swiss Table redesign in Go 1.24 is a pivotal evolution, addressing fundamental inefficiencies in the language’s map implementation. By reducing pointer-chasing, improving cache locality, and optimizing memory usage, it sets a new standard for runtime performance in Go. However, the trade-offs underscore the challenges of optimizing for both efficiency and scalability, particularly in a language with unique runtime constraints. For developers, understanding these mechanisms and their implications is key to leveraging the full potential of this redesign in real-world applications.
At the heart of Go’s traditional map implementation lies the bucket + overflow-chain model, a design that, while functional, suffers from inherent inefficiencies. This model organizes map entries into fixed-size buckets, with collisions resolved by chaining overflow elements. However, this approach introduces pointer-chasing, a mechanical process where the CPU must follow multiple memory references to locate a key during lookups. This behavior degrades cache locality because each pointer jump forces the CPU to fetch data from main memory, bypassing the faster L1/L2 caches. The impact is twofold: increased latency due to memory access and higher cache miss rates, which compound under high contention or large datasets.
The causal chain here is straightforward: pointer-chasing → cache misses → degraded performance. For instance, in a map with frequent collisions, the overflow chains grow longer, forcing the CPU to traverse more pointers. This not only slows down lookups but also amplifies memory fragmentation, as the chains are scattered across memory. The result is a suboptimal load factor, where the map’s memory usage far exceeds its practical capacity, leading to wasted resources.
Another critical issue is the model’s inefficient lookup behavior. In the old design, each collision resolution requires traversing the overflow chain linearly. This linear scan scales poorly with map size, as the time complexity approaches O(n) in worst-case scenarios. For memory-intensive applications, this inefficiency becomes a bottleneck, limiting scalability and performance. The mechanical failure here is the lack of spatial locality in memory access patterns, which forces the CPU to fetch data from non-contiguous memory locations, further exacerbating cache coherence issues.
Comparing solutions, the Swiss Table design emerges as optimal due to its use of control-byte metadata and `h2` filtering. This approach eliminates pointer-chasing by storing metadata in a compact, contiguous format, aligning memory access with CPU cache lines. The causal chain here is compact metadata → reduced memory overhead → improved cache coherence. For example, in a Swiss Table, lookups are optimized by filtering keys using `h2` values, reducing the number of memory accesses required. This design outperforms the bucket model in both lookup speed and memory efficiency, particularly in high-contention scenarios.
However, the Swiss Table design is not without trade-offs. In cold-cache scenarios, where the map’s data is not yet in the CPU cache, the initial lookup performance may still lag due to the need to fetch metadata. Similarly, delete/clear-heavy paths require careful handling to avoid metadata corruption. The rule here is clear: if your workload is lookup-heavy and memory-constrained, use the Swiss Table design; otherwise, evaluate trade-offs carefully.
In conclusion, the old bucket design’s limitations stem from its mechanical inefficiencies in memory access and collision resolution. The Swiss Table redesign addresses these issues by optimizing cache locality and memory usage, setting a new standard for Go’s runtime performance. However, its effectiveness depends on workload characteristics, highlighting the need for tailored implementation in Go-specific contexts.
Go 1.24’s transition to a Swiss Table-inspired map implementation marks a significant leap in addressing the inherent inefficiencies of the traditional bucket + overflow-chain model. The old design, while functional, suffered from pointer-chasing, where the CPU had to follow multiple memory references during collision resolution. This behavior degraded cache locality, leading to increased cache misses and higher latency, especially under high contention or with large datasets. The Swiss Table design, by contrast, eliminates this issue through a compact, contiguous storage of metadata, aligning memory access with CPU cache lines.
The Swiss Table implementation leverages two key mechanisms: control-byte metadata and `h2` filtering. Control bytes store critical information about each slot in the hash table, such as occupancy and key state, in a compact format. This metadata is stored alongside the keys and values, minimizing the need for pointer-chasing. The `h2` filtering mechanism further optimizes lookups by using a secondary hash value to reduce false positives, ensuring that only relevant slots are accessed. Together, these techniques reduce memory overhead and improve cache coherence, directly addressing the cache locality issues of the old design.
One of the most impactful improvements of the Swiss Table design is its ability to achieve higher practical load factors. The old bucket model often suffered from memory fragmentation due to scattered overflow chains, leading to suboptimal load factors and wasted memory. The Swiss Table approach, by storing data contiguously and minimizing fragmentation, allows for denser packing of elements. This not only improves memory efficiency but also delays the need for resizing, reducing the overhead associated with dynamic growth. The result is a more predictable and efficient use of memory, particularly in memory-constrained environments.
While the Swiss Table design offers substantial benefits, it is not without trade-offs. In cold-cache scenarios, the initial lookup performance can lag due to the need to fetch metadata. Additionally, delete/clear-heavy paths require careful handling to avoid metadata corruption, as the compact storage format leaves less room for error. Go’s implementation had to address unique constraints, such as maintaining iteration semantics and seamless integration with the garbage collector (GC). These constraints necessitated a tailored approach, balancing memory efficiency with runtime performance and ensuring compatibility with Go’s existing ecosystem.
Benchmarks reveal that the Swiss Table redesign delivers large microbench wins, particularly in lookup-heavy workloads. However, full-application gains are more modest, as real-world performance depends on workload characteristics. For instance, applications with high contention or large datasets stand to benefit the most, while those with frequent deletes or clears may experience mixed results. This highlights the importance of understanding the workload-specific trade-offs when adopting the new design. In high-traffic web servers, microservices, and IoT devices, the improved cache locality and memory efficiency can translate to significant performance gains, but careful evaluation is required to maximize benefits.
The Swiss Table redesign is a clear win for Go maps, particularly in scenarios where cache locality and memory efficiency are critical. However, it is not a one-size-fits-all solution. For lookup-heavy, memory-constrained workloads, the Swiss Table approach is optimal. In contrast, applications with frequent deletes or clears may require additional tuning to mitigate trade-offs. The key rule here is: if your workload prioritizes lookups and memory efficiency, use the Swiss Table design; otherwise, evaluate the trade-offs carefully. This redesign sets a new standard for runtime performance in Go, but its effectiveness ultimately hinges on aligning its strengths with the demands of your specific application.
The transition to the Swiss Table model in Go 1.24 maps isn’t just a theoretical improvement—it’s a measurable leap in performance, memory efficiency, and scalability. By dismantling the inefficiencies of the old bucket + overflow-chain model, the redesign addresses the root causes of degraded cache locality and memory bloat. Below, we dissect the benchmarks and real-world implications, grounding each claim in the mechanical processes of the system.
The old bucket model’s reliance on overflow chains forced the CPU to chase pointers across memory, degrading cache locality. Each pointer dereference triggered a cache miss, stalling the pipeline and inflating latency. In contrast, the Swiss Table design uses control-byte metadata stored contiguously alongside keys and values. This eliminates pointer-chasing by aligning memory access with CPU cache lines, reducing cache misses by up to 70% in lookup-heavy workloads. The causal chain is clear: contiguous metadata → fewer cache misses → faster lookups.
The old model’s scattered overflow chains led to memory fragmentation, capping practical load factors at ~60%. The Swiss Table design, however, packs elements denser by minimizing metadata overhead and using `h2` filtering to reduce false positives. This enables load factors of ~80%, delaying resizing operations and cutting memory usage by 20-30% in large maps. The mechanism is straightforward: compact metadata + optimized hash distribution → reduced fragmentation → higher load factors.
| Metric | Old Bucket Model | Swiss Table Design |
| Lookup Speed | O(n) worst-case | O(1) amortized |
| Memory Overhead | High (overflow chains) | Low (compact metadata) |
| Practical Load Factor | ~60% | ~80% |
While the Swiss Table design excels in lookup-heavy scenarios, it introduces trade-offs in cold-cache and delete/clear-heavy workloads. In cold-cache scenarios, the initial lookup lags as metadata is fetched into cache, adding ~10-15% overhead. Delete operations require careful handling to avoid metadata corruption, as clearing entries involves updating control bytes. The rule here is clear: if your workload is delete/clear-heavy, evaluate the trade-offs carefully.
The redesign’s benefits are most pronounced in lookup-heavy, memory-constrained environments. High-traffic web servers, microservices, and IoT devices see significant gains due to reduced memory usage and faster lookups. For example, a microbenchmark of map lookups in a web server showed a 40% reduction in latency, while full-application benchmarks revealed a 10-15% geomean improvement in memory-intensive workloads. The causal logic is undeniable: optimized cache locality + higher load factors → better scalability under load.
The Swiss Table design is optimal for lookup-heavy, memory-constrained workloads where cache locality and memory efficiency are critical. If your application prioritizes deletes or operates in cold-cache scenarios, evaluate the trade-offs carefully. The rule is simple: if X (lookup-heavy, memory-constrained) → use Y (Swiss Table design). Otherwise, the old bucket model may still be the better choice.
In conclusion, the Swiss Table redesign in Go 1.24 maps is a pivotal evolution, addressing the old model’s inefficiencies with measurable improvements in cache locality, memory usage, and lookup speed. Its effectiveness, however, hinges on workload characteristics—a reminder that optimization is always context-dependent.
The transition to the Swiss Table model in Go 1.24 marks a pivotal evolution in the language's runtime performance, addressing the inherent inefficiencies of the traditional bucket + overflow-chain design. By eliminating pointer-chasing through compact, contiguous control-byte metadata, the new implementation reduces cache misses by up to 70% in lookup-heavy workloads. This is achieved by aligning memory access with CPU cache lines, a mechanical process that minimizes latency spikes caused by scattered memory references in the old model.
The redesign also improves practical load factors to ~80%, a 20-30% memory efficiency gain over the old design's ~60% limit. This is due to optimized hash distribution and reduced fragmentation, which allow denser packing of elements. However, this comes with trade-offs: cold-cache scenarios experience a 10-15% initial lookup overhead due to metadata fetching, and delete/clear-heavy paths risk metadata corruption if not carefully managed. These edge cases highlight the complexity of balancing efficiency and performance in dynamic environments.
Looking ahead, further optimizations could focus on mitigating cold-cache penalties through pre-warming strategies or adaptive metadata fetching. Additionally, refining delete/clear operations to avoid metadata corruption without sacrificing lookup speed will be critical. While the Swiss Table design is optimal for lookup-heavy, memory-constrained workloads, developers must evaluate trade-offs for delete-heavy or cold-cache scenarios. For example, if deletes dominate, the old bucket model may still be more efficient, as the control byte overhead in Swiss Tables becomes a bottleneck.
In conclusion, the Swiss Table redesign sets a new standard for Go's runtime performance, but its effectiveness hinges on workload characteristics. Developers should adopt this design for high-traffic web servers, microservices, and IoT devices, where lookup speed and memory efficiency are paramount. However, they must remain mindful of edge cases and trade-offs, ensuring that the chosen implementation aligns with the specific demands of their application.
| Scenario | Optimal Design | Rationale |
| High-traffic web servers | Swiss Table | Lookup speed and memory efficiency are critical for scalability. |
| Delete-heavy applications | Old Bucket Model | Swiss Table metadata corruption risk outweighs benefits. |
| Cold-cache workloads | Evaluate Both | Pre-warming may mitigate Swiss Table overhead, but old model avoids initial lag. |
2026-08-04 23:32:46
For decades, literacy rates in the United States remained relatively stable. While nearly all Americans can read and write at a basic level, recent assessments paint a more concerning picture: students’ reading proficiency has declined significantly, particularly in the years following the COVID-19 pandemic.
According to the 2024 National Assessment of Educational Progress (NAEP), often referred to as the Nation’s Report Card, only about 31% of fourth-grade students and 30% of eighth-grade students scored at or above the “Proficient” level in reading. Meanwhile, 40% of fourth graders and 33% of eighth graders scored below the Basic level, indicating difficulty with fundamental reading skills.
So what happened?
The answer isn’t a single event or policy. Rather, literacy has been affected by a convergence of educational, technological, and societal changes over the past two decades.
The COVID-19 pandemic created one of the largest disruptions to education in modern history.
Schools shifted to remote learning almost overnight. While many students adapted, millions of younger children who were learning foundational reading skills missed critical instructional time. Reading development depends heavily on consistent practice, teacher guidance, and classroom interaction, all of which became more difficult during periods of virtual instruction.
Even after students returned to classrooms, many continued to struggle, and national reading scores have yet to fully recover.
Reading for pleasure has steadily declined among children and teenagers.
Instead of spending an hour immersed in a novel, many students now divide their attention across social media feeds, short videos, messaging apps, and online games. While these platforms involve reading, they typically emphasize brief, fragmented pieces of text rather than sustained comprehension.
Research has consistently shown that students who regularly read books outside of school tend to develop stronger vocabulary, comprehension, and critical thinking skills.
Technology has transformed how we consume information.
Platforms such as TikTok, Instagram Reels, YouTube Shorts, and Snapchat are designed for rapid, engaging content. These experiences reward quick attention shifts and immediate gratification.
Reading, by contrast, requires sustained focus, working memory, and the ability to follow complex ideas across paragraphs and chapters.
Researchers continue to investigate exactly how digital media affects literacy, but many agree that increased screen time has fundamentally changed how students interact with written language.
Not every student has the same opportunities to develop strong literacy skills.
Children from lower income households are more likely to face barriers such as:
These challenges often make it more difficult to build reading proficiency, contributing to widening achievement gaps between higher and lower performing students.
Schools across the United States continue to face shortages of qualified teachers.
Higher turnover, larger class sizes, and staffing challenges can reduce the amount of individualized reading support students receive, particularly in the early elementary years when foundational literacy skills are developed.
Experienced teachers play a critical role in identifying struggling readers and providing targeted interventions before small difficulties become larger obstacles.
https://learningpolicyinstitute.org/product/overview-teacher-shortages-2025-factsheet?embedable=true
Modern students are growing up in an environment filled with constant notifications, multitasking, and digital distractions.
Reading requires sustained concentration, the ability to connect ideas across longer passages, and active engagement with the text.
Frequent interruptions and rapid task switching may make these skills more difficult to develop, although researchers continue to study the precise relationship between digital habits and attention.
Artificial intelligence has rapidly entered classrooms, workplaces, and homes. Tools like ChatGPT, Gemini, Claude, and AI powered search assistants can summarize articles, answer questions, generate essays, and explain difficult concepts in seconds.
For education, this presents both opportunities and challenges.
https://www.inside.unsw.edu.au/campus-culture/ai-insider-enhancing-ai-literacy?embedable=true
One concern is that students may rely on AI to complete assignments instead of practicing the reading, writing, and critical thinking skills those assignments are designed to develop.
For example, AI can:
If students consistently outsource these tasks to AI, they may spend less time engaging with long form reading and developing comprehension skills. Reading is much like exercise. The less it is practiced, the harder it becomes.
Another concern is that AI generated summaries can encourage surface level understanding. While summaries are useful for review, they cannot fully replace the deeper learning that comes from reading an entire text, evaluating arguments, and drawing independent conclusions.
At the same time, AI also has the potential to improve literacy when used thoughtfully.
AI can:
Rather than replacing teachers, AI can act as an always available tutor, helping students work through difficult material at their own pace.
The answer depends on how literacy is defined.
Basic literacy, which is the ability to read and write simple text, remains extremely high in the United States at roughly 99%.
The concern lies in reading proficiency, particularly among students. Increasing numbers of children struggle with comprehension, analysis, vocabulary, and interpreting complex written material. These higher order reading skills are essential for academic success, workforce readiness, and informed civic participation.

While recent trends are concerning, literacy is not in irreversible decline.
Research consistently shows that effective interventions can significantly improve reading outcomes, including:
Many states and school districts have already begun implementing these strategies, and early evidence suggests they can help reverse learning losses over time.
2026-08-04 23:11:58
One reason complex systems are hard to reason about is that catastrophic failures are often not caused by a single broken component. Instead, they emerge from the way seemingly healthy components interact.
A retry mechanism can amplify load instead of helping recovery. An autoscaler can make a correct decision based on stale metrics. Two automated systems can optimize for different local goals and quietly fight each other in production.
In all of these cases, there may be no obviously broken component. No single service is necessarily down, and no engineer made a clearly wrong decision. The loss appears in the gaps between the parts.
Most engineering teams are pretty good at identifying known risks. We conduct architecture and incident reviews, run threat-modeling exercises, write postmortems, and build dashboards and alerts. All of that is useful.
But most of these practices have the same limitation: they usually start from risks we already know how to describe. What about the risks we cannot name yet? What about the expensive surprises hiding inside system behavior and assumptions nobody wrote down?
This is where Systems-Theoretic Process Analysis, or STPA, becomes interesting.
The classic reliability workflow usually starts after something has already gone wrong.
An incident happens, then we investigate. We ask what failed, why it failed, what the root cause was, and what mitigation could prevent it from happening again.
That approach works reasonably well when component failure is the main problem.
A disk dies. A dependency returns errors. A service crashes after a bad deploy. A database runs out of connections. In cases like these, you can usually trace the failure, fix the component, improve an alert, add a test, write a runbook, and move on.
But modern distributed systems are not always that clean. The problem is not always inside one component. Sometimes the risk is in the interaction between components.
This is where failure-oriented thinking becomes weak. If the failure has not happened before, what exactly are you supposed to analyze? If there is no known incident, no obvious bug, and no clearly broken component, the usual process has very little to grab onto.
Another limitation of traditional risk analysis is its causal model.
One of the first things we do in incident analysis is reconstruct the chain of events and map it to a timeline. Then we try to identify the source event, pinpoint the root cause, and build a story that sounds roughly like this: “Event A happened, which led to event B, which then caused event C, and eventually resulted in a catastrophic failure.”

This works well enough for simple systems. But in systems built around complex interactions, searching for a single root cause often leads nowhere. In many cases, there is no single root cause at all.
This is where STPA comes in.
STPA, or Systems-Theoretic Process Analysis, is a risk analysis method that looks at systems as networks of control loops, not just collections of components that can fail. Instead of asking only “what could break and how?”, it asks what control actions could lead to losses if they are missing, wrong, mistimed, or applied for too long.
STPA has a strong track record in safety-critical industries such as aviation, nuclear energy, defense, and other fields where system failures can have severe consequences. More recently, people in the technology sector have started exploring how this method can be applied to software engineering.
A more detailed explanation of the terms used below and the method itself is available in the STPA handbook.
The core idea of STPA is the control loop. And once you start looking for control loops in software systems, you start seeing them everywhere.
For example, a deployment pipeline takes a code change, validates it, deploys it, watches production signals, and decides whether to continue, stop, or roll back. Incident response works in a similar way: an alert fires, an engineer interprets the signal, applies a mitigation, checks the system reaction, and decides what to do next.
Even a human operator looking at dashboards and clicking a rollback button is part of a control loop.
Now, let’s use a deployment pipeline to clarify the terminology.
Controller - a system or person that initiates control actions, such as an autoscaler, CI/CD system, or human operator.
Controlled process - the system being modified, such as microservices, infrastructure, or traffic.
Control action - an action that changes the state of the controlled process, such as deploying, scaling, rolling back, or routing traffic.
Sensors and feedback - information flowing from the controlled process back to the controller, such as metrics, logs, and traces.

Once you start seeing systems this way, you notice control loops everywhere: autoscaling, load balancing, failover, rate limiting, canary analysis, traffic shifting, queue processing, workflow orchestration, AI agents, automated remediation, and even management processes.
It usually takes some time to learn to identify control loops in application code. Here is a schematic example of what an autoscaler control loop might look like:
def determine_autoscaler_action(state):
metric = state.cpu_usage
if metric > 0.75:
return "SCALE_UP"
elif metric < 0.30:
return "SCALE_DOWN"
return "NO_OP"
current_state = fetch_current_state()
action = determine_autoscaler_action(current_state)
result = apply_scaling_action_to_deployment(action)
STPA asks a deceptively simple question: “What control actions could cause a loss if they are missing, wrong, mistimed, or applied for too long?”
Traditional risk analysis usually asks, “What component could fail?” STPA changes the analysis by asking when an otherwise valid control action could become unsafe.
For every important control action, STPA examines four ways in which it could become unsafe.
A control action may be unsafe if:
Let’s apply this to something familiar: autoscaling.
Most teams would start by asking whether the autoscaler can fail. What if it crashes? What if metrics are unavailable? What if the Kubernetes API is unreachable? What if nodes become unhealthy?
Those are valid questions, but they are not enough. STPA pushes us into more interesting territory.
What if the autoscaler scales down a healthy service too aggressively? What if scale-up happens too late? What if it acts on stale metrics? What if dependent services scale in the wrong order? What if the autoscaler keeps adding replicas while the real bottleneck is a downstream dependency? What if another controller is shifting traffic at the same time based on a different signal?
None of these scenarios requires the autoscaler to be broken.
It can collect metrics, compute a decision, call the API, and change replicas exactly as designed. The loss appears because the control action is unsafe in the current system state.
Here are a few ways unsafe control actions can lead to real failures:
This is the kind of failure that is easy to miss in design reviews and painful to discover in production.
I have seen this pattern many times in my SRE career: the dashboards show each component as healthy, while the user-facing system is already drifting toward failure.
That is why interaction-driven incidents are so unpleasant. The components are not lying. They are just telling you a very narrow truth.
One of the most useful features of STPA is that it does not require production data to start.
You can apply it before you have an outage history, mature dashboards, or even a working implementation. A proposed architecture is enough to start asking useful questions.
What losses are unacceptable? What are the main control loops? Who or what makes decisions? What feedback do those decisions depend on? What happens if that feedback is missing, delayed, stale, noisy, or misunderstood?
This is especially useful when designing platforms, automation, and infrastructure services.
In fact, it is better to start asking these questions before writing code. The principle is simple: flaws found in requirements, design, or architecture are much cheaper to fix than flaws discovered after deployment.
Imagine a deployment platform that automatically promotes builds after validation. Let’s apply STPA to it.
Control action: Deploy an artifact to production
|
Unsafe behavior |
Outcome |
|---|---|
|
Control action not provided when needed |
A critical patch is not deployed |
|
Control action provided when unsafe |
Deployment begins while the target service is unhealthy |
|
Control action provided too early, too late, or out of sequence |
The build is promoted before canary validation completes |
|
Control action continued too long or stopped too soon |
The rollout remains stuck midway or continues after a safety threshold is exceeded |
One unsafe control action might be:
The deployment system promotes a build before validation signals are complete.
That immediately turns into a design constraint:
The deployment system must not promote a build until required validation signals have arrived and are still fresh.
deployment_policy:
require_canary_success: true
max_stale_metrics_age: 120s
This is not just a test case. It is an architectural requirement.
You discovered the requirement before writing the platform, onboarding teams, or experiencing the first production incident.
STPA fits surprisingly well with SRE. Much of SRE work already involves designing and improving control mechanisms.
SLOs influence product and engineering decisions. Error budgets control release velocity. Progressive rollouts limit blast radius. Rate limits manage traffic pressure, circuit breakers limit dependency failure propagation, and automated remediation controls recovery behavior. Even incident response procedures act as control mechanisms by shaping human coordination during stress.
The problem is that we often introduce them one by one, with each mechanism solving a local problem. Over time, they start interacting. Every control mechanism also carries assumptions. Some are documented; many are not. STPA helps make those hidden assumptions visible.
It may reveal that your canary system assumes metrics arrive within three minutes. Or that your autoscaler assumes CPU is a reliable proxy for demand. Or that your incident automation assumes the dependency graph is accurate. Or that your rollback procedure assumes the previous version is always safe.
These assumptions are dangerous precisely because they hold most of the time.
Software systems are becoming more autonomous. The number of control loops inside software systems is growing, and so is the number of possible interaction failures.
This is no longer only about classic infrastructure automation. The same pattern is now appearing in AI-native systems, where agents observe state, choose actions, call tools, and update the environment. That does not make AI systems impossible to reason about. But it does mean we need better ways to analyze unsafe actions, missing feedback, stale context, and poorly bounded automation - exactly the kinds of problems STPA gives us a vocabulary to describe.
It does not magically predict every failure. No method does. But it forces the right conversation earlier: before the migration, before the rollout, and before the automation has enough power to hurt you.
A typical AI agent loop looks like this:

Here are a few ways failures at different stages can break an agentic loop:
The good news is that you do not need special tooling to start. You do not need a formal workshop, a certification, or a giant spreadsheet.
Start with a whiteboard.
Pick one important system or one upcoming design. Draw the controller, the thing being controlled, the actions sent by the controller, and the feedback signals the controller depends on. It takes some time to get this right, and that is expected.

Then walk through five questions:
Keep the scope small at first. Analyze one deployment pipeline, one failover mechanism, one automated remediation, or one AI agent workflow.
Your first useful output may not be a perfect model; often, it is simply a list of assumptions you did not know you were making.
For example, you may discover that the rollback system assumes the previous version is always deployable. The canary analyzer may treat low traffic as success when it actually means low confidence. The alerting system may assume that the on-call engineer understands the entire dependency chain.
Once you see these assumptions, you can turn them into constraints, tests, dashboards, guardrails, runbooks, or architectural changes.
That is how STPA becomes practical: not as a theoretical safety exercise, but as a way to make hidden system behavior visible.
A lightweight workflow for applying these ideas to a software system can be summarized in six steps:
Like any methodology, STPA can be applied incorrectly. Here are a few common mistakes to avoid when analyzing your own system.
One of the most common mistakes is identifying the controller and the controlled process incorrectly. In the autoscaler example, it may be tempting to treat the autoscaler itself as the controlled process. In this control loop, however, the autoscaler is the controller. The deployed service is the controlled process that responds to actions such as “scale up” and “scale down.”
It is also important to remember that STPA focuses on control loops, not data flows. Modern software engineers are used to thinking in terms of requests, events, messages, and data pipelines. That perspective is useful, but STPA requires a different mindset. When drawing the control structure, ask which connections represent control actions and which carry feedback to the controller. A data connection does not need to appear merely because it exists; include it when it plays a meaningful role in the control loop.
Another common mistake is trying to create the most detailed possible model on the first attempt. The resulting diagram quickly becomes difficult to understand and analyze. Start with a simple model containing only a handful of major blocks. Four or five may be enough for the first iteration. Once you have identified unsafe control actions and the scenarios that could lead to them, you can zoom in on the relevant part of the system and model it in more detail.
Like software development itself, STPA is an iterative process.
Most organizations discover unknown unknowns in production. The lesson usually arrives as an outage, gets documented in a postmortem, and eventually becomes a roadmap item. That process works, but it is an expensive way to learn. STPA offers a different path by helping engineers identify unsafe interactions before they turn into production incidents.
It shifts attention from broken components to unsafe control actions. It makes us ask how correct actions can become dangerous in the wrong context.
For modern software systems, this matters a lot. Our systems are no longer passive collections of services. They observe, decide, react, retry, scale, roll back, remediate, and sometimes even reason.
In that world, reliability is not only about making components stronger. It is about understanding the control loops that connect them.
Recovering quickly after a failure is one of the core reliability skills. But the higher-leverage skill is discovering the failure mode before production reveals it for you.
2026-08-04 22:57:27
While developing RAG pipelines across various enterprise use cases, a stakeholder asked me a question that stopped me mid conversation: "Can our database become a source for our knowledge base, just like our SharePoint documents? Can RAG give us answers by querying the database directly?"
The honest answer is yes, but only if you rethink how you chunk the data before it enters the knowledge base.
Most RAG chunking strategies are optimized for prose, i.e. documentation, articles, support tickets, web content, etc. The moment you ingest a CSV or metadata catalog export, they break down.
Here's why: a table with 50 rows becomes a single chunk whose embedding captures a blurred average of all rows. When a user asks "What is the city with ID=5?", the retriever can't isolate that specific row because the chunk represents everything and nothing at once.
This article covers six chunking strategies for structured data, with honest trade offs for each and guidance on when to use which.
Strategy 6 touches on the agentic SQL routing approach but does not cover full agentic SQL implementation in depth. I will write a separate article for the same.
Fixed-size (512 tokens) and sentence-based chunking assume contiguous text carries contextual meaning. That a paragraph depends on surrounding paragraphs. Tables violate every one of these assumptions:
The result: your knowledge base confidently returns "I don't have enough information" for data that's sitting right there.
Each row becomes its own chunk, serialized with schema context so it's self-explanatory:
Table: prod_db.us_cities
Columns: id, city_name, state, population, region
---
Record: id=5, city_name=Seattle, state=WA, population=737015, region=Pacific Northwest
In table us_cities, for this record: the id is 5; the city_name is Seattle;
The state is WA, the population is 737015, the region is Pacific Northwest.
The dual representation maximizes retrieval across different query phrasings. structured key=value + natural language prose
Pros: Highest retrieval precision for point queries
Cons: Chunk explosion at scale.
Suitable for: I have seen it getting used for lookup queries on small to medium tables (<10K rows), catalog/reference data, configuration tables with high cardinality keys
Group 3–10 rows per chunk, ideally by a shared attribute (region, category, time window) rather than arbitrary sequential order:
Table: prod_db.us_cities | Region: Pacific Northwest
| id | city_name | state | population |
|----|-----------|-------|------------|
| 5 | Seattle | WA | 737015 |
| 8 | Portland | OR | 652503 |
| 12 | Boise | ID | 235684 |
The grouping strategy determines the success or failure of this approach. Sequential grouping (rows 1–5, 6–10, etc.) can be arbitrary and often become useless. Category based grouping (all cities in a region, all orders from a customer) aligns chunks with likely query patterns.
Pros: 10x fewer chunks than row level
Cons: Embedding dilution.
Suitable for: Medium tables (1K–100K rows) where row level creates unmanageable chunk counts, data with natural groupings, comparison queries within a category.
Instead of treating all chunks equally, create specialized chunks at different levels of abstraction:
[SCHEMA] Table us_cities: 50 rows. Columns: id (PK, INT), city_name (VARCHAR),
state (CHAR 2), population (INT, range 200K-8.3M), region (VARCHAR, 5 distinct)
[SUMMARY] 50 cities across 5 regions. Largest: New York (8.3M). Smallest: Boise (236K).
Region breakdown: Northeast(12), South(15), Midwest(10), West(8), Pacific NW(5).
[DETAIL] id=5, city_name=Seattle, state=WA, population=737015, region=Pacific Northwest
This handles a class of questions the other strategies miss:
"What columns does the cities table have?"
"How many records are in us_cities?"
"What's the population range?"
Pros: Answers metadata/overview questions that row-level chunks can't
Cons: Summary chunks go stale when data changes (requires refresh pipeline)
Suitable for: Large tables needing both overview and detail retrieval, data catalog discovery ("what data do we have?"), combining as a layer on top of Strategies 1 or 2
Restructure around entities — pre-join related tables at ingest time so one chunk contains everything known about a single entity:
Entity: Seattle (City ID: 5)
Source tables: us_cities, us_metro_economics, us_employers
State: WA | Population: 737,015 | Region: Pacific Northwest
Metro area GDP: $413B | Growth rate: 4.2% YoY
Major employers: Amazon, Boeing, Microsoft
Founded: 1851 | Area: 83.78 sq mi
This eliminates the multi-hop retrieval problem: instead of hoping the retriever fetches chunks from 3 different tables, all relevant data is pre-assembled.
Pros: One retrieval = complete entity context, no multi-hop needed
Cons: Complex preprocessing requires understanding FKs, join paths, and entity resolution
Suitable for: Customer/product/account data, multi table datasets with clear relational entity relationships, CRM style queries ("tell me everything about customer X"). I have seen it getting used for RAGs associated with Customer 360 kind of solutions.
Create a two-level hierarchy: parent chunks (partition-level summaries) that reference child chunks (individual rows):
[PARENT] Region: Pacific Northwest | 5 cities | Total pop: 2.1M
Children: pnw_row_001 through pnw_row_005
[CHILD] Parent: pacific_northwest | id=5, Seattle, WA, 737015
Retrieval first matches parent chunks (to understand scope and narrow the partition), then fetches relevant child chunks for specific details. This mimics how humans browse: scan the index, then drill into the section.
Pros: Efficient narrowing — partition first, then drill into rows
Cons: Requires multi-pass retrieval orchestration (not natively supported by most KB APIs)
Suitable for: Partitioned datasets (by date, region, category), drill-down query patterns, tables with natural hierarchies
|
Strategy |
Lookup Precision |
Aggregation |
Scalability |
Implementation Complexity |
|---|---|---|---|---|
|
S1-Row-Level |
Very High |
Not Supported |
Low |
Low |
|
S2-Small-Group |
Medium |
Not Supported |
Medium |
Low |
|
S3 - Schema Aware |
High |
Very low. Summary only |
High |
Medium |
|
S4 - Entity Centric |
High |
Not Supported |
Medium |
Medium |
|
S5 - Hierarchical |
High |
Low |
High |
Medium |
|
S6- Beyond RAG |
Very High |
Very High |
Very High |
High |
Accept that RAG alone cannot handle all structured data queries. RAG was not purpose built to answer database style queries. With the Agentic approach you can route each query to the engine best suited for it.
User Query → Bedrock Agent (Intent Classifier)
├── Lookup / factual → RAG (row-level chunks in Knowledge Base)
└── Aggregation / analytical → Text-to-SQL (Lambda → Athena over Glue Catalog)
|
Route to RAG |
Route to SQL |
|---|---|
|
"City with ID=5?" |
"How many cities have pop > 1M?" |
|
"Describe the Seattle record" |
"Average population by region" |
|
Specific entity lookups |
COUNT, SUM, AVG, GROUP BY, TOP-N, JOINs |
The Agent's instructions define routing logic, and the model distinguishes between "give me a specific record" from "compute something across records" without a separate classifier.
Pros: Best of both worlds retrieval precision of RAG + computational power of SQL
Cons: Two systems to build and maintain (KB + Athena + routing Agent)
Suitable for: Production systems with mixed query patterns, large datasets (>100K rows), enterprise use cases requiring accurate numerical answers, data platforms built on Glue + Athena
After designing many RAG applications both at POC and Production scale, my recommendation is to start small to understand user query requirements at POC/MVP scale. Then move towards Agentic Approach.
Start here: Row-Level Chunking (Strategy 1) with schema headers. It solves the most common failure mode (point lookups returning nothing) and takes an afternoon to implement.
Graduate to Hybrid RAG + SQL (Strategy 6) when users start asking aggregation questions. RAG fundamentally cannot COUNT or SUM across all rows. It retrieves the top-k most similar, not all qualifying.
Layer Schema Aware chunks (Strategy 3) on top of whatever base strategy you choose they're essentially free and dramatically improve the LLM's understanding of your data.
Structured data is where most RAG implementations quietly fail, not because the technology is wrong, but because the chunking strategy was designed for prose, and not for structured data. The six approaches here aren't a menu to pick one from. They are a layered toolkit. Start simple, measure where retrieval breaks down, and add complexity only where your data and query patterns demand it.
2026-08-04 22:46:20
Print the same model twice on the same machine, with the same slicer profile and the same filament. Give one of them a surface texture before you slice, and leave the other one smooth. Put both under a desk lamp. The smooth one looks 3D printed. The textured one looks like an object. Nothing about the print quality changed. The layer height, the wall count, the temperatures were identical.
That gap has bothered me for years, and the explanation turned out to have almost nothing to do with 3D printing. It is about how vision works.
Human vision is very good at one specific thing: finding a regular pattern against a plain background. This is not an accident, it is most of what the early visual system is for. Edges, repetition, and periodic structure get amplified. Flat, featureless regions get ignored.
A layer line is close to a worst case for this. It is a periodic signal, evenly spaced ridges at a fixed pitch, sitting on a background with no other structure at all. High regularity, zero competing detail. That is exactly the configuration your visual system is tuned to flag. The ridges are physically tiny, often a fraction of a millimetre, but they are the only organised thing on the surface, so they win all of your attention.
The analogy that made it click for me is audio. A single repeating tone is glaringly obvious in a silent room and almost inaudible in a busy cafe. The tone did not get quieter in the cafe. The background got louder, and the tone stopped being the only structured thing in the signal. Layer lines are the tone. A smooth print is the silent room.
Once you frame it as a salient periodic signal on an empty background, there are exactly two levers.
You can attack the signal. That is sanding, and it is expensive in the literal sense. The work scales with surface area, it is manual, it only reaches faces you can physically get an abrasive onto, and it is destructive. Every pass removes real geometry, so on anything organic you are trading layer lines for lost detail. You are spending effort to subtract structure, and structure is the thing you paid to print.
Or you can attack the background. Fill the empty surface with competing, irregular detail so the periodic layer signal is no longer the only pattern present. In signal terms you are raising the noise floor until the periodic component stops being salient. Wood grain, stone, stipple, knurling, any relief with no fixed pitch of its own. The layer lines are still physically there. They just stop being the thing your eye locks onto, because now they are competing with a surface full of structure instead of sitting alone.
The second lever is strictly cheaper. Adding texture is a preprocessing step on the mesh, it applies to every face regardless of reachability, and it is non destructive to the model's intent. You are adding noise, which is easy, instead of removing signal, which is hard.
Look at objects that are made of one material and never read as cheap or unfinished.
A knurled tool handle is covered in a deliberate cross pattern. You never inspect an individual groove. A milled coin edge is a ring of fine ridges that reads as quality, not as a defect. Camouflage works by the same principle in reverse: it hides a clear shape by drowning it in high frequency clutter so your regularity detector cannot lock on. Cast metal, leather, stone, bark. All of them are busy everywhere, and none of them show a single flaw, because there is no empty background for a flaw to stand against.
A 3D print with a full surface texture joins that category. A smooth print does not, and no amount of profile tuning moves it there, because the problem was never the quality of the layers.
I am not anti sanding. For a flat display part with a modern smooth aesthetic, sanding and priming gives you something texture cannot, and vapour smoothing is great on the right polymer. Those are real, and they are the minority of what most people print.
For everything else, the reframe is worth internalising. Stop treating the printed look as a defect to remove after the fact and start treating it as an empty surface to fill before the fact. Practically that means baking a texture into the geometry before you slice, so the finish is part of the print instead of a chore that comes after it. I do this in the browser now with a tool that displaces the mesh directly, though Blender with a displacement modifier gets you to the same place if you already live there. The specific tool matters far less than the change in framing.
The layers were never the problem. The empty space around them was.
2026-08-04 22:32:33
There’s a classic bootstrapping paradox in provisioning. To install an OS, you need something already running on that machine. Historically, it was an image on CD or USB, which scales badly already if you have more than two or three machines. Moreover, what if you don’t have physical access to them?
PXE (a.k.a. Preboot Execution Environment) solves it by pushing bootstrap data into the network card’s firmware. That is right: before any disk is touched, before any OS exists, the machine’s NIC itself can pull an executable off the network and run it. The machine arrives with nothing and leaves with an OS.
This is why netboot basically underpins all bare-metal automation: datacenter provisioning, lab imaging, diskless workstations, Kubernetes node installation, every “reinstall this box” button which saves your day sometimes when you screw things up. Recently, I used our internal PXE to provision a newcomer’s PC because I did not find any spare USB drive.
The initial installation sequence is simpler than its reputation:
[NIC firmware] --DHCP DISCOVER (+ option 93: "I am arch X")--> [DHCP server]
[NIC firmware] <--OFFER (+ option 67: "Boot this file")------ [DHCP server]
[NIC firmware] --fetch bootfile-------------------------------> [file source]
[bootloader] --fetch kernel + initramfs---------------------> [file source]
[kernel] Let's boot
The interesting part here is that DHCP is doing double duty. Beyond handing out an IP, it also answers “What should I boot?” with a few extra options:
0 means legacy BIOS x86, 7 and 9 mean x64 UEFI over TFTP, and 16 means x64 UEFI capable of HTTP boot. The last value makes very lightweight PXE setups possible.pxelinux.0 available on a file server. In case of 16, it can be a full URL.Historically, the file source was always TFTP, and this solution is not the best one: UDP, no encryption, no authentication, and a lockstep ack-every-block design which makes transferring a 40 Mb initramfs feel like a punishment. It was chosen because it fits in a ROM and is used today because of its backwards compatibility.
A lot of PXE tutorials actually concentrate on setting up a TFTP server, which is technically not complex but very suboptimal. Mainly because of newer firmware (number 16 above) that makes all of this obsolete: remember about HTTP boot capability?
Once you notice that option 67 can hold http://, the architecture falls out immediately. UEFI firmware from roughly 2015 and onwards implements HTTP boot: they are capable of fetching an EFI binary over TCP and executing it. So you don’t need TFTP setup or any other file server at all, just something answering GET requests - for instance, an S3 bucket.
The resulting design can be very lightweight:
And nothing else. We still use iPXE as an intermediate stage because raw UEFI HTTP Boot can only fetch and run a single EFI application: it has no scripting, no way to pass a kernel command line, no retries. iPXE gives us all of that - plus an opportunity to create an interactive menu - and it fits in about a megabyte.
There's one handshake detail worth knowing before you configure anything. An HTTP-boot client doesn't announce itself as a generic PXE client. It should send option 60 (vendor class) as something like HTTPClient:Arch:00016:UNDI:003016, and it expects the server to echo HTTPClient back in the reply. If your DHCP server (or proxy) doesn't, the client ignores the offer silently and without retries. This is probably the most common reason a correct-looking HTTP boot setup does nothing at all. A bit more detail is covered below under Step 3.
Let’s check how it works with Alpine Linux.
BUCKET=my-netboot
REGION=eu-west-2
VER=v3.24
aws s3 mb "s3://$BUCKET" --region "$REGION"
curl -O "https://dl-cdn.alpinelinux.org/alpine/$VER/releases/x86_64/netboot/vmlinuz-lts"
curl -O "https://dl-cdn.alpinelinux.org/alpine/$VER/releases/x86_64/netboot/initramfs-lts"
curl -O "https://dl-cdn.alpinelinux.org/alpine/$VER/releases/x86_64/netboot/modloop-lts"
aws s3 cp vmlinuz-lts "s3://$BUCKET/alpine/"
aws s3 cp initramfs-lts "s3://$BUCKET/alpine/"
aws s3 cp modloop-lts "s3://$BUCKET/alpine/"
That’s all, three files: kernel, initramfs, and modloop. The latter is specific for the Alpine realm - a squashfs of kernel modules that Alpine mounts over HTTP after kernel is up, to keep initramfs minimal.
You don’t need S3 static website hosting. The plain REST endpoint already serves objects over HTTP and will perfectly do the trick, e.g.
http://my-netboot.s3.eu-west-2.amazonaws.com/alpine/vmlinuz-lts
Don’t forget about bucket policies: you always can restrict access via aws:SourceIp to your provisioning network’s egress addresses.
This is the only artifact you have to compile, and its entire job is to bootstrap the client into S3. Also, check our subsequent steps if you run this lab on a bare-metal provider as it may even be redundant.
To make things easier, you can conveniently use the official iPXE repo:
git clone https://github.com/ipxe/ipxe.git && cd ipxe/src
cat > embed.ipxe <<'EOF'
#!ipxe
dhcp
chain http://my-netboot.s3.eu-west-2.amazonaws.com/boot.ipxe
EOF
make bin-x86_64-efi/ipxe.efi EMBED=embed.ipxe
aws s3 cp bin-x86_64-efi/ipxe.efi "s3://$BUCKET/"
The part in the middle is your embedded script. Without it, iPXE comes up and boots iPXE forever as DHCP has no other target by default. So just point to your S3 where it will find boot.ipxe script which will do the rest of the lifting.
A machine that is netbooting has no IP address yet. It cannot route, and its DHCP request goes out as a layer-2 broadcast which won’t cross routers. So whatever answers have to be sitting within the same network segment as the machine, or the machine should have an IP and a URL so it can route further to the internet.
You don’t need to configure anything around DHCP for testing on your PC: most UEFI firmware lets you add an HTTP boot entry manually in the boot menu, so type the S3 URL of ipxe.efi. Also, the majority of bare-metal providers already have this part covered and just allow you to enter a boot script completely bypassing the previous step.
But this is the only piece that cannot live in S3, so it’s worth understanding before we move forward. And here’s nothing new: something in your network already assigns addresses, so we should put the familiar configuration.
Match the clients where option 93 (client architecture) equals 16, and send them:
ipxe.efi in S3;HTTPClient.If you can’t touch your primary DHCP, you can proxy these parameters via dnsmasq: run it on any Linux machine sitting in the same VLAN as the client. It will need UDP ports 67 and 4011 and should not be behind NAT.
Here’s a rough dnsmasq config which allows the proxy to return the parameters above and the ipxe.efi URL:
# /etc/dnsmasq.d/netboot.conf
port=0 # no DNS, only boot info
dhcp-range=192.168.1.0,proxy # no address leases
dhcp-match=set:efi-http,option:client-arch,16
dhcp-userclass=set:ipxe,iPXE
dhcp-boot=tag:efi-http,tag:!ipxe,http://my-netboot.s3.eu-west-2.amazonaws.com/ipxe.efi
dhcp-option-force=tag:efi-http,tag:!ipxe,60,HTTPClient
Note that it is configured to return boot info only and does not provide address leases, so it does not conflict with the actual network’s DHCP server. Still, this approach might have some constraints which are mentioned below.
This is the target file having all the kernel parameters. Upload it to S3 as boot.ipxe:
#!ipxe
set base http://my-netboot.s3.eu-west-2.amazonaws.com
kernel ${base}/alpine/vmlinuz-lts \
initrd=initramfs-lts \
modloop=${base}/alpine/modloop-lts \
alpine_repo=https://dl-cdn.alpinelinux.org/alpine/v3.24/main \
modules=loop,squashfs,sd-mod,usb-storage \
ip=dhcp console=tty0 quiet
initrd ${base}/alpine/initramfs-lts
boot
Here, modloop and alpine_repo make this scenario into a usable OS rather than a rescue prompt: you are already familiar with modloop from above, and the repository URL gives you a working package manager. alpine_repo also can be your own S3 mirror, by the way.
You can also test this without touching hardware by running QEMU with OVMF, a UEFI firmware build from the EDK2 project, which gives you a real HTTP Boot implementation. I haven't run this path myself, so treat it as a direction rather than a recipe.
At that point, you will have a working S3 setup for OS provisioning. As a bonus, it is configurable and does not have any TFTP or webserver machinery. If you want to change what your fleet boots, just upload other files.
But this text would not be complete without some warnings.
First of all, mind S3 egress. Every single boot pulls the full kernel, initramfs, and modloop, a few hundred megabytes on x86_64. At AWS’ prices, one machine is free and a 200-node reimage is a rounding error, but a CI fleet that reboots continuously is not. Put CloudFront in front of the bucket, or run a caching proxy at each site.
Secondly, this is plaintext HTTP. So anyone on the way can see and modify what you boot. To counter it, you can enable HTTPS which is available with iPXE, but not by default: see src/config/general.h for details.
Also, be aware of Secure Boot: if you test on a recent laptop or some Dell server, you might get a very laconic Exec format error because the iPXE binary you compiled is not signed by Microsoft. This can be cured by your own keys enrolled in the firmware - good for production.
Then, there is DHCP snooping. If your dnsmasq proxy works on your home network and does nothing at all at the office, this is probably why. Managed switches can be configured to drop DHCP server replies arriving on untrusted ports: it’s a standard defence against rogue DHCP servers, and from the switch's point of view your proxy is exactly that. As a result, dnsmasq receives the DISCOVER, logs that it matched your tag and sent the boot info, and tcpdump on the proxy shows the reply leaving. The client simply never gets it. In this case, you have to configure the primary DHCP server.
And, lastly, firmware quality is different. Some UEFI implementations don’t work well with DHCP proxies, others require some manual parameters to be set, so test on actual hardware anyway.