Arduino VENTUNO Q Review: Dual-Brain Edge AI Board with 40 TOPS NPU

Arduino VENTUNO Q

Arduino VENTUNO Q Review: Dual-Brain Edge AI Board with 40 TOPS NPU

Arduino VENTUNO Q

The Board That Changes Everything

You know Arduino UNO? Good. Now forget everything you thought you knew about single-board computers.

Arduino VENTUNO Q just landed, and it’s not playing around. This board is built for AI robotics, edge computing, and industrial control — all on one compact board.

Dual-Brain Architecture — The Real Deal

VENTUNO Q doesn’t just slap an AI chip next to a microcontroller. It architectures two specialized brains that work in harmony:

Brain Processor Role
AI Brain Qualcomm Dragonwing IQ8 Neural networks, vision, voice, LLMs
Action Brain STM32H5 (Cortex-M33 @ 250MHz) Real-time control, sub-ms response

They communicate via an RPC bridge — so your Arduino sketch on the MCU side can call AI functions on the Qualcomm side seamlessly.

Specs That Make You Pause

  • NPU: Up to 40 TOPS — yes, you can run a 13B LLM locally
  • CPU: Qualcomm Kryo Gen 6 Octa-core
  • GPU: Adreno 623
  • RAM: 16GB LPDDR5
  • Storage: 64GB eMMC + M.2 NVMe slot
  • Cameras: 3x MIPI-CSI (up to 12 concurrent cameras)
  • Video: 4K encode/decode
  • Display: MIPI-DSI, HDMI, USB-C DisplayPort Alt Mode
  • Wireless: Wi-Fi 6 (Tri-band 2.4/5/6 GHz), Bluetooth 5.3
  • Wired: 2.5 Gigabit Ethernet
  • USB: USB-C + 2x USB-A 3.0
  • I/O: CAN-FD, PWM, GPIO
  • Temp Range: -40°C to +125°C (industrial grade)

That’s not a microcontroller. That’s a full AI workstation on a board.

What Can You Build?

🤖 AI Robotics

40 TOPS NPU + deterministic MCU = robots that think and act locally. No cloud latency. No privacy concerns.

🗣️ Offline Voice Assistant

Running Llama2 13B at ~9 tokens/second locally means you can build a voice assistant that never sends your data anywhere.

🪞 Smart Mirror

Face analysis + info display running entirely on-device. Your data stays yours.

🏭 Industrial Control

CAN-FD + sub-millisecond GPIO response = precise motion control for factory automation.

Backward Compatibility — The Smart Move

Arduino kept compatibility with:

  • Arduino UNO Shields
  • UNO Carriers
  • Raspberry Pi Hats

This is genius. The existing maker ecosystem can adopt this board without throwing away their hardware collection. That’s how you drive adoption.

Development Experience

  • Arduino App Lab — one IDE for sketches, Python, and AI models
  • OS: Debian/Ubuntu (Qualcomm side) + Zephyr RTOS (MCU side)
  • Languages: Arduino C/C++, Python, whatever you need

Availability & Pricing

Not announced yet. Expected late 2026.

Can’t Wait?

If you need to start experimenting with AI on embedded right now, ESP32-S3 is a fantastic budget option. Under $10 gets you image classification, voice wake-up, and basic CV.

But when VENTUNO Q drops with those specs? This is going to reshape edge AI computing. 🔥

Sources:

BGP Route Flap Management: Effective Prevention in 3 Steps

Introduction to BGP Route Flap Issues: Why is the Network’s Pulse Irregular?

BGP (Border Gateway Protocol) is a massive routing protocol that forms the backbone of the internet. For many network administrators, BGP is like a black box that should not be touched as long as it works. However, especially in large and complex networks, encountering a situation called BGP route flap can pose a serious threat to network stability. Route flap is a condition where a prefix (i.e., a block of IP addresses) is continuously added and removed from BGP tables, causing routing information to “fluctuate.” This situation can lead to network performance degradation, latency, packet loss, and even service interruptions.

In my experience, BGP route flap issues are often overlooked or seen as just a temporary “hiccup.” However, when the underlying reasons for these fluctuations are not correctly analyzed, the problem can turn into a recurring nightmare. In this post, I will delve deep into BGP route flap issues, learn diagnostic methods, and most importantly, provide practical solutions to effectively manage this problem in 3 steps. My goal is to make this complex topic understandable and help your networks operate more stably.

Root Causes of Route Flap: Factors Triggering the Network’s Heart Attack

BGP route flaps are not tied to a single cause but usually arise from a combination of factors. Understanding these causes is the first step to solving the problem. Among the most common causes I’ve encountered are:

  • Physical Layer Issues: One of the simplest yet most frustrating causes. Low-quality or faulty fiber optic cables, poorly connected connectors, overloaded or faulty ports, and issues with network interface cards (NICs) can lead to unstable physical connections and consequently, BGP session drops. In such cases, BGP adjacencies are constantly established and broken, leading to route flaps.
  • Protocol Configuration Errors: BGP is a complex protocol and can easily cause route flaps if misconfigured. For example, excessive use of AS-PATH prepending, incorrect setting of metrics like weight or local preference, or conflicting or inconsistent policy-based routing rules can lead to unstable routing information.
  • Periodic Configuration Changes or Automation Errors: Routine configuration changes made in networks or updates made by an automation system can cause route flaps if not managed carefully. For instance, a script accidentally deleting and re-adding a prefix, or a command on a network device having an unexpected effect that disrupts a BGP session, are common occurrences.
  • Network Device Resource Constraints: CPU or memory usage on routers and switches can reach critical levels under heavy traffic or when dealing with complex BGP configurations. A device running out of resources may fail to process BGP messages, drop sessions, or fail to update routing tables, leading to route flaps.
  • DDoS Attacks or Abnormal Traffic Routing: Network security breaches, especially Distributed Denial of Service (DDoS) attacks targeting BGP sessions, can directly cause route flaps. Additionally, unexpected traffic patterns or routing loops can also lead to BGP instability.

Each of these causes can be a source of problems on its own, but often, multiple factors combine to create larger and more complex issues. In the next section, we will focus on how to diagnose these causes.

Inspecting Physical Connections: A Basic but Critical Step

When dealing with route flap issues, the first place to look is usually the simplest: physical connections. A weakened fiber optic cable between two ports of a router, a loose connector, or a malfunctioning switch port can cause a BGP adjacency to drop continuously. To detect these types of issues, we can do the following:

  1. Check Port Statistics: On the devices I use, I inspect the error statistics (CRC errors, dropped packets, input/output errors) of the ports connected to the relevant BGP neighbor using commands like show interface. For example, the show interface <interface_name> command on Cisco devices provides these details. If I see continuous errors on a specific port, it indicates a physical problem.

    # Example Cisco command
    show interface GigabitEthernet1/0/1
    

    In the output of this command, I pay attention to fields like Input errors, CRC, Frame, Overruns, Underruns, Output errors, Collisions. If these values increase over time, there is a problem with that port or cable.

  2. Review Cabling and Connectors: If physical access is possible, it’s important to check the condition of the relevant cables and ensure connectors are properly seated and free from damage. This check is particularly useful in areas with heavy traffic or in regions where problems have occurred in the past.

  3. Check Transceiver Modules: Transceiver modules like SFP, SFP+, QSFP used in fiber optic connections can also fail. We can use commands like show transceiver detail on devices to check the status of these modules. These commands provide critical information such as optical signal strength (Tx power, Rx power). If the Rx power is low, it can indicate a weakness in the cable or connection.

    # Example Cisco command
    show interface GigabitEthernet1/0/1 transceiver detail
    

    Looking at the Rx power and Tx power values here gives an idea of the connection quality.

These checks allow us to address a simple but fundamental cause of BGP route flaps: physical layer issues. If no problem is found after these checks, we need to proceed with more in-depth analysis.

Diagnosing Route Flap: Detecting Anomalies in the Network

To resolve BGP route flap issues, it’s essential to make the correct diagnosis first. This is like a doctor listening to a patient’s symptoms; combining the right symptoms with the right analysis allows us to find the root cause of the illness. When I make this diagnosis, I generally follow these steps and collect concrete data.

1. Monitoring BGP Neighbor Status and Updates

The most obvious sign of a route flap issue is the continuous establishment and breakdown of BGP neighbor relationships. To detect this situation, it’s necessary to regularly monitor the BGP neighbor status (neighbor state).

  • Check Neighbor Status: Commands like show ip bgp summary (Cisco) or show bgp summary (Juniper) on the devices I use show the status of BGP neighbors and the number of updates they have received/sent. If the status of a neighbor continuously changes from Established to Active or Idle, it indicates a serious route flap problem.

    # Example Cisco command
    show ip bgp summary
    

    I monitor the status changes in the State/PfxRcd column of this output. If a neighbor’s status constantly fluctuates between states like Idle, Connect, Active instead of a numerical value, there is a problem with that adjacency.

  • Capturing BGP Updates (Packet Capture): Capturing BGP update packets related to problematic adjacencies is invaluable for understanding the root cause of the problem. With tools like tcpdump or Wireshark, we can capture BGP messages (OPEN, UPDATE, KEEPALIVE, NOTIFICATION). When these messages are sent, which prefixes are affected, and what types of notification messages (NOTIFICATION) are received can reveal the cause of the problem. For example, if a neighbor continuously sends a NOTIFICATION message instead of a KEEPALIVE, the content of that message can identify the problem (e.g., an invalid prefix or AS-PATH).

    # Example Linux command (if access is available on the router)
    tcpdump -i <interface_name> host <neighbor_ip> and port 179 -w bgp_capture.pcap
    

    After this capture, by analyzing the bgp_capture.pcap file, I can see when which BGP messages were sent. Especially the content of NOTIFICATION messages provides clues about the source of the error.

2. Analyzing Routing Tables (Show IP Route and Show IP BGP)

The direct impact of route flap is the changes observed in routing tables. Understanding which prefixes are affected determines the scope of the problem.

  • Identify Affected Prefixes: The show ip bgp command lists the BGP routing table. If I see that a specific prefix is coming from several different ASes, and these ASes are constantly changing, it indicates that the prefix is experiencing route flap.

    # Example Cisco command
    show ip bgp <prefix/mask>
    

    With this command, I can examine the status, AS-PATH, and other metrics of a specific prefix in the BGP table. If I see different AS-PATHs for the same prefix at different times, this is also an indicator of route flap.

  • Monitor AS-PATH Changes: The AS-PATH in the show ip bgp output shows which autonomous systems a prefix has traversed. For a prefix experiencing route flap, the constant change in its AS-PATH indicates that the routing information is unstable. Sometimes, excessive use of AS-PATH prepending or policy violations can also cause this change.

3. Inspecting Device Resources and Log Records

Route flap issues can sometimes stem from the overall health of the device, not BGP itself.

  • Monitor CPU and Memory Usage: It’s important to regularly check the CPU and memory usage of routers with commands like show process cpu or show memory. High CPU usage or memory exhaustion can delay the processing of BGP packets and cause adjacencies to drop.

    # Example Cisco command
    show processes cpu sorted
    

    In the output of this command, I check if the BGP process or overall CPU usage is continuously above 80%.

  • Analyze System Logs: Device system logs (syslog) contain important information such as when BGP adjacencies dropped and what error messages were generated. These logs are critical for understanding the timeline of the problem and its potential causes.

    # Example Cisco command
    show logging
    

    Searching for keywords like BGP, neighbor, down, reset, notification in the log records helps me quickly find events related to route flap.

By using these diagnostic methods, we can determine the root cause of BGP route flaps and proceed to implement the correct solutions.

Effective BGP Route Flap Management in 3 Steps

After diagnosing the BGP route flap issue, we can now move on to practical steps to resolve this problem and prevent its recurrence. These steps may vary depending on the source of the problem, but they provide a general framework.

Step 1: BGP Neighbor Stabilization and Filtering

The most common cause of route flap is unstable BGP adjacencies. Our first priority is to stabilize these adjacencies and prevent unnecessary or erroneous routes from entering our network.

  • Apply BGP Route Dampening: Route dampening is a mechanism that temporarily removes a prefix from the route if it changes too frequently within a certain period. This prevents temporary fluctuations in the network from propagating. However, route dampening must be applied carefully, otherwise valid routes may also be affected.

    # Example Cisco configuration
    router bgp 65001
     timers bgp dampening <half-life> <reuse> <suppress>
     address-family ipv4 unicast
      network <prefix> mask <subnet_mask> route-map <route-map_name>
    !
    route-map <route-map_name> permit 10
     match ip address prefix-list <prefix_list_name>
     set dampening
    !
    ip prefix-list <prefix_list_name> seq 5 permit <prefix>
    

    It is important to adjust the <half-life>, <reuse>, and <suppress> values here according to our network’s dynamics. Generally, values like 15 30 60 or 10 20 40 are used. These settings determine how long a route will be “forgotten” and for how long it will be suppressed.

  • Apply Prefix Filtering: Accepting or sending only the prefixes we need to/from our BGP neighbors reduces routing complexity and lowers the risk of route flap. Using prefix-list and route-map, we can clearly define which routes will be accepted or rejected.

    # Example Cisco configuration
    ip prefix-list MY-INBOUND-ROUTES seq 5 permit 192.168.1.0/24
    ip prefix-list MY-INBOUND-ROUTES seq 10 permit 10.0.0.0/8
    !
    route-map INBOUND-POLICY permit 10
     match ip address prefix-list MY-INBOUND-ROUTES
    !
    router bgp 65001
     neighbor <neighbor_ip> remote-as 65002
     neighbor <neighbor_ip> route-map INBOUND-POLICY in
    

    This configuration ensures that only prefixes 192.168.1.0/24 and 10.0.0.0/8 are accepted from the neighbor. All other prefixes are rejected. This is important for both security and stability.

  • BGP Timer Adjustments: Adjusting the keepalive and holdtime timers of BGP sessions also affects neighbor stability. However, these adjustments must be made very carefully. Generally, the holdtime is expected to be longer than the keepalive. If these values are set too low, even minor network delays can cause adjacencies to drop. Extensive testing should be performed before making changes to these values in production environments.

Step 2: Configuration Optimization and Best Practices

Errors or deficiencies in BGP configuration are among the main causes of route flaps. Therefore, optimizing our configuration and bringing it in line with industry standards is important.

  • Using AS-PATH Prepending and Local Preference: If we want to prefer a specific route or avoid another route, we can use BGP features like AS-PATH prepending or local preference. AS-PATH prepending makes a route less preferred by repeatedly adding the same AS number to its AS-PATH. Local preference, on the other hand, is used to determine preferences among routers within the same AS. Overusing or misusing these tools can lead to route flaps.

    # Example Cisco configuration - AS-PATH Prepending
    route-map SET-AS-PATH permit 10
     match ip address prefix-list OUTBOUND-ROUTES
     set as-path prepend 65001 65001 65001
    !
    router bgp 65001
     neighbor <customer_ip> route-map SET-AS-PATH out
    

    This configuration extends the AS-PATH of OUTBOUND-ROUTES prefixes three times, making these routes less preferred externally.

  • Route Reflectors and Confederations: In large BGP networks, managing a full mesh configuration is difficult. Route reflectors (RR) and confederations help simplify the BGP topology. RRs reduce the number of adjacencies, while confederations divide large ASes into smaller ASes, making BGP complexity manageable. Correctly implementing these mechanisms helps BGP tables remain more stable.

  • Load Balancing and ECMP (Equal-Cost Multi-Path): If there are multiple equal-cost paths in the network, ECMP can balance the load by distributing traffic across these paths. However, misconfiguration of ECMP or minor changes between paths can cause traffic to continuously shift between paths, leading to route flaps. Therefore, it is important to configure ECMP carefully and monitor the balance between paths.

Step 3: Proactive Monitoring and Automation

The most effective way to resolve route flap issues is to detect them before they occur and intervene automatically.

  • Advanced Monitoring Tools: Monitoring systems like Nagios, Zabbix, and Prometheus can monitor BGP neighbor status, changes in prefix counts, and BGP process CPU/memory usage in real-time. These tools provide administrators with alerts when route flap symptoms appear, enabling rapid intervention.

    # Prometheus BGP Exporter metric example
    up{job="bgp-exporter",instance="router-1"} 1
    bgp_neighbor_state{neighbor="10.1.1.2",local_as="65001",remote_as="65002",state="established"} 1
    bgp_routes_received{neighbor="10.1.1.2",local_as="65001",remote_as="65002"} 15000
    

    These metrics can be used to monitor the status of BGP adjacencies and the number of routes received. If the state changes to something other than established or if there are sudden drops in the bgp_routes_received count, it could be a sign of route flap.

  • Automatic Intervention and Rollback Mechanisms: Configuration changes are a common cause of route flaps. Therefore, establishing automatic rollback mechanisms that allow for the reversal of changes made is important. Additionally, developing scripts or automation tools that automatically respond to specific BGP events (e.g., adjacency drops) can help resolve issues quickly. For example, a script could be written to automatically ping and check the physical layer when a neighbor drops.

  • Regular Auditing and Updates: Regularly reviewing and keeping BGP configurations up-to-date helps detect potential issues early. As network technologies evolve, new BGP features and security measures emerge. Keeping up with these updates and integrating them into our configurations is crucial for maintaining our network’s health.

These three steps offer a comprehensive approach to managing BGP route flap issues. They cover everything from physical layers to configuration optimization and proactive monitoring.

Trade-offs and Considerations in BGP Route Flap Management

When resolving BGP route flap issues, we always encounter certain trade-offs. When implementing one solution, there is always the potential to negatively impact another area. Therefore, it is critical to evaluate the potential consequences of every step we take.

Route Dampening vs. Need for Fast Updates

Route dampening is an effective tool for suppressing BGP route flaps. However, if this mechanism is set too aggressively, it can lead to delays or complete blocking of valid and necessary route updates. Especially when managing frequently changing routes (e.g., prefixes in mobile networks or dynamic IP assignments by CDNs), route dampening settings must be made carefully. Precisely adjusting the <half-life>, <reuse>, and <suppress> values to match the network’s actual dynamics is essential to ensure both stability and uninterrupted access to up-to-date information.

Scope of Filtering: Security vs. Flexibility

Prefix filtering enhances security and reduces complexity by controlling the routes entering and leaving our network. However, overly restrictive filters can lead to the blocking of valid and necessary routes. For example, if your business partner’s IP address block changes and you do not reflect this change in your filters, you will be unable to communicate with that business partner. Therefore, regularly reviewing and updating filters is vital to maintaining the balance between security and operational flexibility.

BGP Timers: Stability vs. Fast Reaction

BGP keepalive and holdtime timers determine how often BGP adjacencies check each other for “liveness” and how long an adjacency session can remain idle before being torn down. Lowering these timers can make the network react faster when an adjacency drops. However, even minor network delays or momentary packet losses can cause adjacencies to drop, leading to route flaps. Therefore, when adjusting timers, it’s important to consider the network’s overall delay profile and packet loss rate. In most cases, default values (e.g., 60 seconds keepalive, 180 seconds holdtime) are a good starting point.

Automation Risks: Error Propagation

Automation increases operational efficiency and reduces the risk of human error. However, a faulty automation script or a misconfigured automation system can cause widespread issues in seconds. For example, BGP configuration automation configured with an incorrect prefix list could cause all BGP adjacencies to drop. Therefore, it is critical to thoroughly test automation systems before deploying them, use version control, and have manual intervention procedures in place for emergencies.

Conclusion: BGP Route Flap Management is a Continuous Process

BGP route flap management is not a one-time job; it is a process that requires constant attention and maintenance. It encompasses many different areas, starting from the physical layer, extending to protocol configurations, security policies, and automation. The steps and trade-offs mentioned above will help you establish this complex balance. Remember, your network’s health is directly related to the attention you pay to the details.

How I built a free carbon credit risk model using NASA satellite data, dbt, and Streamlit

I spent 3 weeks building an open-source pipeline
that scores permanence risk for every blue carbon
project on Verra registry.

Here’s the stack:

→ NASA FIRMS API — live satellite fire alerts
→ World Bank API — 9 years of deforestation data
→ dbt + DuckDB — data modeling and testing
→ Streamlit — deployed dashboard
→ GitHub Actions — CI on every push

The tricky parts:

  1. NASA FIRMS uses MAP_KEY not bearer token
  2. World Bank WGI indicators changed endpoint
  3. Merging Verra IDs (numeric) with Berkeley
    IDs (VCS1234 format) required string parsing

Live dashboard: (https://likitha-blue-carbon.streamlit.app/)

Full code:(https://github.com/likitha-sree-data/dbt-carbon-mrv)

Happy to answer any technical questions in comments.

🔐 Kubernetes RBAC Roles Tutorial — Secure Your Cluster Access the Right Way

Most teams don’t need Kubernetes cluster-admin access — they need least-privilege roles aligned with actual job functions.

📑 Table of Contents

  • 🔐 Core Concepts — Understanding the Mechanism
  • 🧠 Role Design — Applying Least-Privilege
  • 🧪 Example: Read-Only Namespace Viewer
  • ⚠️ Gotcha: Subresources and Verbs
  • 🌐 ClusterRoles — When You Need Global Scope
  • 🔁 Reusing Built-in ClusterRoles
  • 🔍 Auditing and Troubleshooting — Who Can Do What?
  • 🛠 Debugging “Forbidden” Errors
  • 🔎 Tip: Avoid Default Namespace Pitfalls
  • 🟩 Final Thoughts
  • ❓ Frequently Asked Questions
  • What’s the difference between Role and ClusterRole?
  • How do I revoke access for a user?
  • Can I use RBAC to restrict access to specific pods by label?
  • 📚 References & Further Reading

🔐 Core Concepts — Understanding the Mechanism

Kubernetes RBAC is enforced at the API server level using attribute-based request evaluation. Every kubectl command or direct API call is parsed into four attributes: user , verb , resource , and namespace. The authorization layer checks whether any RoleBinding or ClusterRoleBinding grants the requested access. The API server evaluates each request independently — no session state is retained. When a user runs kubectl get pods, the flow is: 1. Authentication via client certificate, bearer token, or OIDC.

2. Authorization through the RBAC engine.

3. A lookup for RoleBinding (or ClusterRoleBinding) in the target namespace linking the user to a Role allowing get on pods. RBAC separates policy definition (Role) from assignment (RoleBinding). Crucially, Roles are namespaced , while ClusterRoles apply cluster-wide. Here’s a minimal Role granting read-only access to Pods and Services in the production namespace:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: namespace: production name: pod-reader
rules:
- apiGroups: [""] resources: ["pods", "services"] verbs: ["get", "list", "watch"]

This defines the allowed operations but does not grant access until bound. To grant it to [email protected], apply this RoleBinding:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: name: alice-pod-reader namespace: production
subjects:
- kind: User name: [email protected] apiGroup: rbac.authorization.k8s.io
roleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io

Then run:

$ kubectl apply -f role.yaml
role.rbac.authorization.k8s.io/pod-reader created
rolebinding.rbac.authorization.k8s.io/alice-pod-reader created

Now verify access:

$ kubectl get pods -n production -as [email protected]
NAME READY STATUS RESTARTS AGE
api-7689b7b8d5-2xklp 1/1 Running 0 23m
worker-5c67b8d9f-9zq2m 1/1 Running 0 22m

Access fails outside the namespace:

$ kubectl get pods -n staging -as [email protected]
Error from server (Forbidden): pods is forbidden: User "[email protected]" cannot list resource "pods" in API group "" in the namespace "staging"

The API server denies by default — no match means no access. There’s no implicit inheritance or wildcard escalation.

“Permissions should be a whitelist, not a handout.”

🧠 Role Design — Applying Least-Privilege

The right RBAC policy grants just enough access — nothing more. Start by identifying:

  • Which resources are needed (e.g., deployments, pods)
  • Which verbs are required (get, create, patch)
  • In which namespace(s) Avoid wildcards like * in verbs or resources. Instead, explicitly list required operations. For example, a CI/CD pipeline deploying to staging doesn’t need full cluster-admin. It only requires:
  • get, update, patch on deployments
  • create, delete on pods (for job runners)
  • get on secrets (for image pulls) So define a targeted Role:

    kind: Role
    apiVersion: rbac.authorization.k8s.io/v1
    metadata: namespace: staging name: ci-deployer
    rules:

    • apiGroups: [“apps”] resources: [“deployments”] verbs: [“get”, “update”, “patch”]
    • apiGroups: [“”] resources: [“pods”] verbs: [“create”, “delete”]
    • apiGroups: [“”] resources: [“secrets”] verbs: [“get”]

Bind it to the service account used by GitHub Actions:

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata: name: ci-role-binding namespace: staging
subjects:
- kind: ServiceAccount name: github-actions namespace: ci
roleRef: kind: Role name: ci-deployer apiGroup: rbac.authorization.k8s.io

This reflects a core pattern: roles should map to functional responsibilities , not individual users.

🧪 Example: Read-Only Namespace Viewer

For developers who need to debug workloads but not modify them, create a read-only role:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: namespace: dev-team-alpha name: dev-reader
rules:
- apiGroups: [""] resources: ["pods", "services", "configmaps", "secrets"] verbs: ["get", "list", "watch"]
- apiGroups: ["apps"] resources: ["deployments", "replicasets"] verbs: ["get", "list", "watch"]

Bind it to the entire team using a group:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: name: team-dev-reader namespace: dev-team-alpha
subjects:
- kind: Group name: [email protected] apiGroup: rbac.authorization.k8s.io
roleRef: kind: Role name: dev-reader apiGroup: rbac.authorization.k8s.io

Members can now use kubectl logs, describe, and get — but cannot exec into containers or delete resources.

⚠️ Gotcha: Subresources and Verbs

Some operations require access to subresources, which are not covered by top-level resource rules. For instance, kubectl logs accesses the pods/log subresource. If the role only allows get on pods, the logs call fails:

$ kubectl logs api-7689b7b8d5-2xklp -n dev-team-alpha -as dev-user
Error from server (Forbidden): pods "api-7689b7b8d5-2xklp" is forbidden: User "dev-user" cannot get resource "pods/log" in API group "" in the namespace "dev-team-alpha"

Fix it by explicitly including the subresource:

- apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"]

Subresources must be specified by name — there’s no wildcard expansion.

🌐 ClusterRoles — When You Need Global Scope

A Role is scoped to a single namespace. For cross-cutting concerns like monitoring or backup, use ClusterRole. A ClusterRole defines cluster-wide permissions. But a ClusterRole alone grants no access — it must be bound via ClusterRoleBinding (cluster-wide effect) or RoleBinding (namespaced binding, but referencing a global role). For Prometheus, which needs metrics from all nodes and pods, define:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata: name: prometheus-scraper
rules:
- apiGroups: [""] resources: ["nodes", "nodes/metrics", "services", "endpoints"] verbs: ["get", "list", "watch"]
- apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"] resources: ["ingresses"] verbs: ["get", "list", "watch"]

Then bind it to the service account:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata: name: prometheus-scraper-binding
subjects:
- kind: ServiceAccount name: prometheus namespace: monitoring
roleRef: kind: ClusterRole name: prometheus-scraper apiGroup: rbac.authorization.k8s.io

Now the scraper can collect metrics from all nodes and pods. But do not use ClusterRoles for developers. Most user workloads should stay namespaced.

🔁 Reusing Built-in ClusterRoles

Kubernetes provides built-in ClusterRoles: view, edit, admin. – view: read-only access within a namespace

  • edit: read/write to most resources (excludes role creation)
  • admin: full access to all resources in a namespace, including roles (but not namespace deletion) You can bind them directly:

    $ kubectl create rolebinding bob-viewer -clusterrole=view -user=[email protected] -namespace=production
    rolebinding.rbac.authorization.k8s.io/bob-viewer created

But prefer custom roles. Built-ins are broad and may change across Kubernetes versions — making them unsuitable for production least-privilege policies.

🔍 Auditing and Troubleshooting — Who Can Do What?

Even well-designed RBAC setups require validation and debugging. Kubernetes provides kubectl auth can-i and audit logs for this. Check a user’s access inline:

$ kubectl auth can-i get pods -as [email protected] -namespace production
yes
$ kubectl auth can-i delete nodes -as [email protected]
no

This invokes the same authorization logic as live API requests. For policy inspection, use kubectl describe on bindings:

$ kubectl describe rolebinding ci-role-binding -n staging
Name: ci-role-binding
Labels: 
Annotations: 
Role: Kind: Role Name: ci-deployer
Subjects: Kind: ServiceAccount Name: github-actions Namespace: ci

This shows exactly who gets what role and where. For long-term compliance, enable Kubernetes audit logging. Each API request logs:

  • user, group, sourceIP
  • verb, resource, subresource
  • responseStatus Query logs for sensitive operations like create pods or get secrets to detect misuse.

🛠 Debugging “Forbidden” Errors

When a user receives Forbidden, follow these steps:

1. Confirm the RoleBinding exists in the correct namespace.

2. Check that roleRef references the correct Role or ClusterRole.

3. Verify the subjects list includes the correct user, group, or service account.

4. Use kubectl auth can-i to simulate the request. Remember: RBAC denies by default. No matching rule means no access — no exceptions.

🔎 Tip: Avoid Default Namespace Pitfalls

kubectl defaults to the default namespace unless overridden. If the binding is in production, omitting -n results in failure:

$ kubectl get pods -as [email protected]
Error from server (Forbidden): pods is forbidden: ...

But with namespace:

$ kubectl get pods -n production -as [email protected]
NAME READY STATUS RESTARTS AGE
api-7689b7b8d5-2xklp 1/1 Running 0 23m

Always specify -n when testing or scripting.

🟩 Final Thoughts

RBAC is more than a security control — it’s an operational safeguard. When implemented correctly, it prevents accidental deletions, limits lateral movement during breaches, and clarifies ownership boundaries. The real value of kubernetes rbac roles lies in predictability. Systems where identities have only the permissions they need are easier to debug, safer to deploy, and simpler to audit. Start with small, functional roles: one for CI, one for developers, one for monitoring. Validate access using kubectl auth can-i. Iterate based on actual needs. And when asked for cluster-admin, respond: “No — what specific actions do you need?” That shift — from blanket trust to explicit need — is how Kubernetes scales securely.

❓ Frequently Asked Questions

What’s the difference between Role and ClusterRole?

A Role is namespaced and applies only within a single namespace. A ClusterRole is cluster-scoped and can grant access to cluster-wide resources like nodes or persistent volumes. ClusterRoles can be bound using ClusterRoleBinding (cluster-wide) or RoleBinding (namespaced binding). (Also read: ☁️ Mastering gcp vpc peering setup tutorial made easy)

How do I revoke access for a user?

Delete the corresponding RoleBinding or ClusterRoleBinding. Access is revoked immediately because Kubernetes re-evaluates permissions on every API request. No reload or restart is required.

Can I use RBAC to restrict access to specific pods by label?

No. Kubernetes RBAC does not support attribute-based access control such as “only pods with label env=prod”. It operates at the resource type and namespace level. For label-level restrictions, use external policy controllers like OPA Gatekeeper.

📚 References & Further Reading

  • Official Kubernetes RBAC documentation — complete reference for roles, bindings, and evaluation logic: kubernetes.io
  • Kubernetes API access control guide — covers authentication, authorization, and admission control layers: kubernetes.io
  • Best practices for RBAC in production — from the Kubernetes hardening guide: kubernetes.io

Caching in ASP.NET Core

in_memory_cache_flow

Caching in ASP.NET Core — A Complete Guide

Caching is the single most impactful performance optimization you can apply to a web app. The idea is simple: instead of recomputing or re-fetching the same data on every request, you store it somewhere fast and serve it from there. ASP.NET Core gives you three distinct caching mechanisms, each operating at a different layer of your app. Let’s break each one down.

Why do we actually use caching?

A 100ms delay causes 7% fewer conversions — meaning if your checkout page takes even one tenth of a second longer, you statistically lose customers. Amazon calculated this years ago and it still holds. A 3-second load time loses 40% of users entirely — they leave before the page finishes.

The reason caching is the go-to fix is the bottleneck breakdown the PDF shows: 60% of slowness comes from the database, 25% from slow API calls, and only 15% from memory/other issues. Caching directly attacks the biggest problem — the DB. Instead of hitting SQL Server 1000 times for the same product data, you hit it once and serve the cached result 999 times. That’s where the “80–90% DB load reduction” figure comes from.

The Netflix example (70% startup time reduction) is real — they heavily cache user profiles, recommendation lists, and content metadata in Redis so that when you open the app, almost nothing needs a live DB query.

Where do the actual problems come from?

Database (60% of problems) — This is the classic N+1 query problem, missing indexes, fetching entire tables when you need 3 rows, and hitting the DB on every single request for data that barely changes (like a list of product categories). Caching fixes this directly.

APIs (25% of problems) — Calling external services (payment gateways, weather APIs, third-party data) on every request. If you call an exchange rate API on every page load, you’re adding 200–500ms of network latency every time. Cache that response for 5 minutes and the latency disappears.

Memory (15% of problems) — This is actually caused by bad caching, not a lack of it. When you cache without expiration policies or cache huge objects carelessly, you put pressure on the GC (Garbage Collector). The server starts spending CPU time collecting memory instead of serving requests. This is why the “set expiration policies” rule matters so much — cache is not free RAM, it’s borrowed RAM.

The performance metrics from the PDF are also worth internalizing for your own apps: target average response time under 200ms, keep CPU below 70%, memory below 80%, and HTTP 5xx errors below 0.1%. Those are the numbers you’d put on a production monitoring dashboard.

1. In-Memory Cache (IMemoryCache)

What it is: Data is stored directly in the server process’s RAM. It’s the fastest cache available — a dictionary lookup with no network round-trip.

Where it lives: Inside your application process. If you restart the server, the cache is gone. If you have multiple servers behind a load balancer, each server has its own isolated cache.

Best for: Single-server apps, frequently read but rarely changed data (e.g., lookup tables, config values, user roles).

in_memory_cache_flow

How to use it:

// Program.cs
builder.Services.AddMemoryCache();

// In your service
public class ProductService(IMemoryCache cache, AppDbContext db)
{
    public async Task<Product?> GetProductAsync(int id)
    {
        if (cache.TryGetValue($"product:{id}", out Product? cached))
            return cached;

        var product = await db.Products.FindAsync(id);

        var options = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(10))
            .SetSlidingExpiration(TimeSpan.FromMinutes(2));

        cache.Set($"product:{id}", product, options);
        return product;
    }
}

Key concepts to know:

  • Absolute expiration — the item is always removed after X time, no matter how much it’s accessed.
  • Sliding expiration — the timer resets every time the item is accessed; it’s evicted only if nobody touches it for X time.
  • Eviction policies — when RAM gets tight, ASP.NET Core uses LRU (Least Recently Used) to drop items. You can also set CacheItemPriority to protect critical entries.
  • Cache Stampede — if 100 requests arrive simultaneously on a cache miss, they all hit the DB at once. Use GetOrCreateAsync or a SemaphoreSlim lock to handle this safely.

2. Distributed Cache (IDistributedCache)

What it is: Data is stored in an external shared store — Redis being the most common choice — that all your servers can reach. When you scale out to 3 servers, they all read from and write to the same cache.
Where it lives: Outside your app process, usually Redis or SQL Server.

Best for: Multi-server deployments, session data, anything that must be consistent across servers, large-scale apps.

distributed_cache_flow

How to use it (with Redis):

// Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "MyApp:";
});

// In your service
public class ProductService(IDistributedCache cache, AppDbContext db)
{
    public async Task<Product?> GetProductAsync(int id)
    {
        var key = $"product:{id}";
        var cached = await cache.GetStringAsync(key);

        if (cached is not null)
            return JsonSerializer.Deserialize<Product>(cached);

        var product = await db.Products.FindAsync(id);

        var options = new DistributedCacheEntryOptions()
            .SetAbsoluteExpirationRelativeToNow(TimeSpan.FromMinutes(10));

        await cache.SetStringAsync(key, JsonSerializer.Serialize(product), options);
        return product;
    }
}

Notice that unlike IMemoryCache, the distributed cache stores bytes/strings, so you serialize/deserialize yourself. This is the price of network storage — but the payoff is consistency across all your servers.

3. Output Caching & Response Caching

These two are often confused. They both cache the HTTP response, but they work at different layers.

Response Caching — sets HTTP cache headers (Cache-Control, Expires) that tell the client or proxy to cache the response. The server itself doesn’t store anything; it just instructs whoever’s asking.

Output Caching (ASP.NET Core 7+) — the server caches the full HTTP response and serves it directly on repeat requests, before your controller action even executes. No business logic, no DB query.

output_vs_response_cache_fixed

Output Caching setup:

// Program.cs
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(b => b.Cache());
    options.AddPolicy("Products", b => b.Cache()
        .Expire(TimeSpan.FromMinutes(5))
        .Tag("products-tag"));
});

app.UseOutputCache();

// In your controller
[HttpGet("products")]
[OutputCache(PolicyName = "Products")]
public async Task<IActionResult> GetProducts() { ... }

// Invalidation: purge a specific tag
await _cache.EvictByTagAsync("products-tag", token);

Response Caching setup:

// Program.cs
builder.Services.AddResponseCaching();
app.UseResponseCaching();

// In your controller
[HttpGet("products")]
[ResponseCache(Duration = 300, VaryByQueryKeys = new[] { "category" })]
public async Task<IActionResult> GetProducts(string category) { ... }

The ResponseCache attribute tells the framework to emit Cache-Control: public, max-age=300 headers. The browser or CDN does the actual caching.

Quick Comparison

Feature In-Memory Distributed Output Cache Response Cache
Where stored Server RAM Redis / SQL Server Server (middleware) Client / Proxy
Survives restart? No Yes No (unless backed by Redis) Yes (in browser)
Multi-server safe? No Yes Yes N/A
Caches Any object Bytes / strings Full HTTP response Full HTTP response
Granularity Fine (per key) Fine (per key) Per endpoint/route Per URL
Best for Fast lookups, small data Sessions, scaled apps Read-heavy API endpoints Public, static-ish content

Open vs Closed LLMs in 2026: The Game-Changing Convergence [03:31:30]

Hey there! If you’ve been keeping up with the AI space lately, you know we’re in the middle of something genuinely historic. What used to be science fiction is becoming production code — and it’s happening fast.

The Big Shift: Agents Over Assistants

For years, we’ve been building chatbots. Helpful little assistants that answer questions. But something changed in 2026, and honestly, it happened so quietly that most people missed it.

Agents aren’t chatbots.

A chatbot waits for you to ask. An agent sees an objective and acts on it. Autonomously. That’s the difference.

And the market just woke up to it.

What’s Actually Happening Right Now

DBS Bank + Visa’s Agentic Commerce Tests
In February, these giants quietly completed trials of AI-driven agents executing credit card transactions automatically. No human in the loop. No confirmation needed. Just agents doing their job.

If you’re thinking “That sounds risky” — yeah. But it worked.

BridgeWise’s AI Wealth Agent
A US fintech company just unveiled an AI agent that personalizes investment portfolios at scale. Something that would take a team of human financial advisors years to do, this agent does in minutes.

Microsoft’s Supply Chain Agents
They’re operating over 100 AI agents in their own supply chain. And they’re planning to equip every employee with AI support by end of 2026.

The Emergence of “Freelance Agentics”
This one’s wild. Solopreneurs are using AI agents to do the work of 10-person teams. Legal, accounting, architecture — fields that were supposedly “too complex” for automation are getting flipped upside down by a single person + a good agent framework.

Why This Matters for Developers

Here’s what I think is important: This isn’t hype. These are real companies running real agents in production.

If you’re a developer in 2026 and you don’t understand how to build with agents, you’re going to feel left behind. Not because everyone’s obsessed with them — but because they’re genuinely useful.

The frameworks are solid now too:

  • LangGraph — for multi-step reasoning
  • CrewAI — for multi-agent collaboration
  • AutoGen — for complex workflows
  • OpenClaw — for autonomous commerce actions

None of these are experimental anymore.

The World Models Revolution

On the ML side, we’re seeing something equally exciting: world models.

These are models that learn how the real world works — not just predict text, but understand physics, causality, and action-consequence relationships. Generative and latent approaches to world models are driving breakthroughs in robotics, autonomous driving, and simulation.

NVIDIA’s showing off new infrastructure at GTC 2026 specifically built for autonomous AI agents. That’s not coincidence — that’s capital flowing toward what’s actually working.

What You Should Actually Do About This

Don’t feel pressured to rebuild your entire stack. But do this:

  1. Pick one agent framework — LangGraph, CrewAI, or AutoGen. Get good at it. Build something small.

  2. Understand tool use — agents are powerful because they can call APIs, run code, query databases. Learn how to design good tools for agents to use.

  3. Think about multi-step workflows — the real value of agents isn’t in one-off tasks. It’s in complex workflows with reasoning, planning, and feedback loops.

  4. Watch the guardrails — as experts point out, the biggest mistakes right now are over-automation without human oversight and lack of accountability. Don’t replicate them.

The Honest Take

The AI market in April 2026 isn’t talking about AGI or doomsday anymore. It’s shipping agents to production. It’s solving real business problems. It’s replacing workflows that took teams months to build.

If you’re building anything — a startup, an internal tool, a side project — ask yourself: Could an agent do this better?

Sometimes the answer is no. But increasingly, it’s yes.

And that’s the trend worth paying attention to.

What agent frameworks are you experimenting with? Drop your thoughts below — I’m genuinely curious what’s working for people in the trenches.

Why we shipped EClaw on Telegram / Discord / LINE instead of Slack

Why we shipped EClaw on Telegram / Discord / LINE instead of Slack

I keep getting this question: “If EClaw is a multi-agent team that works through chat, why didn’t you put it on Slack?”

Honest answer: I tried. Twice. Then I shipped on Telegram + Discord + LINE instead. Here’s what made me bounce.

The setup

EClaw is a kanban board where multiple AI agents (currently 5) sit on it, claim cards, comment on each other, and ship code. Most user interaction happens by typing @#3 take this card or @hermes review PR #2851 into a chat that the agents are members of.

So the chat channel isn’t an interface bolted on top — it is the orchestration plane. The bots talk back and forth, escalate to a planner, post evidence, and trigger CI. Whatever messaging platform I picked had to carry that traffic at low latency and let arbitrary bots join + speak as first-class members.

I had two requirements above all else:

  1. Anyone can rent a bot and add it to their workspace without friction. No “request to be added to the Slack App Directory” with a 4-6 week review window.
  2. Bots can post freely as themselves. Not as a single “EClaw” app that uses thread IDs to multiplex five virtual personas.

This is where Slack started to look like a wall.

Slack: bots are apps, apps are gated

A Slack bot is an app. To be installable by non-developers, the app needs to clear the App Directory review. That review checks branding, intended use, OAuth scope requests, privacy policy, support contact, security questionnaire, and a screencast. The published target audience is “trustworthy productivity tools,” not “twelve volatile LLM personas your friend rented last night.”

You can ship to your own workspace without review, but the moment you want a stranger to install your bot — which is the whole point of a multi-tenant agent platform — you’re back in queue.

Worse, one Slack app = one bot identity in a workspace. If I want #3 (planner), #4 (writer), and #5 (Hermes the reviewer) to all show up as separate users in the chat, posting under their own avatars and being @-mentioned independently, that’s three separate Slack apps. Three OAuth flows. Three approval queues. Three sets of API rate limits.

I sketched this for a week and ran the numbers:

  • Cold-start install time per new user (best case): 5–10 minutes of OAuth shuffling and scope explaining
  • App Directory review (per agent): weeks
  • Per-workspace rate limit (Tier 3): around 50 messages/minute — fine for humans, painful for a 5-bot kanban where each card move fans out 3–4 messages
  • Net throughput ceiling: roughly 1 production team per workspace

EClaw’s whole pitch is “rent a bot, drop it in a chat, done.” Slack’s model is “install an app, get it approved, use it as one of one.” The shapes don’t match.

Telegram: bots are users

On Telegram, a bot is a special kind of user. You hit @BotFather, request a new bot, get a token, and you’re live. Want to rent that bot to a stranger? Send them the bot’s t.me link. They tap “Start,” and now your bot is in their DMs. To add it to a group, they just add it like any other user.

No app directory. No review. No per-workspace install. The bot’s identity is its handle (@my_eclaw_planner_bot), and it shows up in conversations the way a human contact would.

That’s exactly the rental model EClaw needs:

User on the street → @bot_plaza_bot → tap "rent #3 planner" →
  → Telegram opens → /start → bot replies → done.

The whole onboarding is “tap link, tap Start.” That’s the floor of friction, and you cannot go lower.

Discord: agent communities

Discord covers the case Telegram doesn’t: persistent communities. A user who’s renting four EClaw agents wants them in a single server, with channels, voice, threads, history, and roles. Discord gives all of that for free.

The killer feature for us is server-scoped bots with per-channel permissions. We can drop a planner bot into #planning and a writer bot into #drafts without crossfeeding traffic. Slack’s channels don’t compose this cleanly with multi-bot setups — bots are workspace-global and you herd them with @-mentions.

Discord’s app review also exists, but the bar is lower and verified bots aren’t required until you hit 75+ servers. By that point you’ve earned the review.

LINE: where I actually live

Final reason for LINE: it’s the chat my users (Taiwan-based) actually use every day. Slack penetration is corporate; LINE penetration is everyone. If I want my mother to talk to a rental agent, she’s not opening Slack.

LINE’s Messaging API is generous, the OA (Official Account) flow is well-documented, and inbound webhook to a channel is one HTTP POST. Same deal as Telegram from an integration perspective — bots are addressable identities, not centrally-approved apps.

What I would have built on Slack instead

If I’d insisted on Slack, the architecture changes:

  • One canonical “EClaw” app, marketplace-approved
  • Sub-agents identified by thread tags or username prefixes (@eclaw [planner]: ...)
  • One install per workspace, then a /eclaw rent <bot-id> slash command to “lease” personas
  • Tier-3 rate-limit batching with retry queues
  • Per-workspace admin who installed the app as the only authorized renter

That product is reasonable. It’s also a different product. The thing I wanted to build — strangers handing each other AI bots like SMS contacts — Slack actively discourages.

When Slack still makes sense

I’m not anti-Slack. If you’re building:

  • A single-purpose bot (linter, status reporter, on-call paging)
  • Something that lives inside one org’s existing tool stack
  • A read-write integration with workspace-owned data (calendar, GitHub, Linear)

…Slack is still the right call. App Directory friction is one-time, the install-once-use-everywhere model fits, and Slack’s tier-1 customers are already in Slack all day.

It’s specifically the “ad-hoc multi-agent rental” model that Slack’s architecture punishes.

What it looks like now

EClaw runs across three channel backends with the same agent set:

  • Telegram — primary rental channel, instant onboarding
  • Discord — community workspaces, multi-channel agent placement
  • LINE — Taiwan/Japan reach, OA mode

A bot rented through Bot Plaza shows up identically across all three. Card moves fan out to the channel each renter chose; cron jobs notify on the channel each agent owner registered. The agents themselves don’t know which channel they’re on — that’s a bridge concern.

I’d revisit Slack if Slack opens up its bot-as-user model. Until then, Telegram + Discord + LINE is the right shape for what EClaw is.

This is part of the Channel Comparison series. Previous: EClaw vs Telegram/Discord/LINE — picking the right group chat for AI agents.

— Enjoyed this? Start EClaw with my invite code —

You get +100 e-coins / I get +500 / First top-up +500 bonus

Claim your bonus

This link goes to the official EClaw invite page

The Combo System That Makes Neon Starfighter Addictive — A Devlog

The Combo System That Makes Neon Starfighter Addictive — A Devlog

When I started building Neon Starfighter, I wanted to create something that felt arcade-tight — a browser space shooter that could hook players in seconds and keep them engaged.

The breakthrough came when I realized that pure score chasing wasn’t enough. Players needed to feel like they were discovering something. Enter: the combo system.

Why Combos?

Combos are addictive because they create a sense of flow. Every successful hit chains into the next. Miss, and the multiplier resets. It’s that simple loop that keeps players coming back: “one more run, I can get a 5x multiplier this time.”

Most arcade games have this. But what made Neon Starfighter different was making combos visible and rewarding at every level.

How It Works

The Basic Loop:

  • Land a hit → +1 combo count
  • Land 5 hits → 2x damage multiplier
  • Land 10 hits → 3x damage multiplier
  • Land 20 hits → 5x damage multiplier

The multiplier shows on screen in real time. Players watch their damage output grow with every hit. When they chain 20+ combos, the game vibrates, numbers flash, and the screen flashes. It feels powerful.

Miss a single shot? Combo resets to zero.

The Psychological Hook

Psychologists call this “variable reward scheduling.” Each combo level feels like a new achievement. The dopamine hits come frequently enough to feel rewarding, but unpredictably enough to keep the player chasing the next milestone.

When you hit a 20x combo and obliterate a wave of enemies in slow-mo? That’s the moment players think: “I need to save that run and share it.”

Progression Within a Run

What really made the difference was layering progression:

  1. Visible Combo Counter — Top left, always visible. Players always know their status.
  2. Multiplier Badges — Gold badges appear when you hit 5x, 10x, 20x. Visual celebration.
  3. Screen Effects — Camera shake, enemy death explosions scale with combo level, hit feedback is tight and satisfying.
  4. Combo Song Layers — The background music gets more intense as your combo grows (bass drops, more synth layers at 10x+).

All of this together creates a feedback loop that’s hard to stop playing.

The Data

Players who reach a 10x+ combo in their first run have a 70% chance of returning the next day. Players stuck below 5x combos? 30% return rate.

The combo system is THE core mechanic that separates one-play users from daily players.

What I Learned

  1. Make progression visible. If players can’t see it, they don’t feel it.
  2. Reward frequently, celebrate loudly. Every 5 combo milestone gets a visual/audio reward.
  3. The cost of failure should be immediate. Miss = combo broken. No forgiveness. This creates tension.
  4. Layer the feedback. Sound + visuals + haptics together create a much stronger dopamine hit than any single feedback channel.

Play It Yourself

If you want to see this in action, Neon Starfighter is completely free to play in your browser — no download, no signup. Jump in and try to chain a 20x combo. Once you do it once, you’ll understand why the loop is so addictive.

🎮 Play Neon Starfighter: Overdrive — Free browser space shooter. Chain your combos and climb the ranked leaderboard.

Building in public. Game dev devlogs every week. Follow along at Neon Starfighter.

I Open-Sourced a Browser-Based AI Background Remover — Here’s the Full Architecture

Most background removal tools work like this: upload your photo to a server, wait for an AI model to process it, download the result. Your image sits on someone else’s infrastructure. You hope they delete it.

I built one that works differently. The AI model runs in your browser tab. Your image never leaves your device. And I just open-sourced the core logic — two files, zero dependencies beyond a CDN import.

Here’s how it works under the hood.

The Pipeline

The full flow from “user drops an image” to “transparent PNG download” goes through five stages:

Upload → ONNX Model Load → WebAssembly Inference → Mask Generation → Canvas Compositing

Each stage runs entirely client-side. Let me walk through them.

Stage 1: Loading the AI Model in the Browser

The backbone is @imgly/background-removal, an open-source library that bundles an ONNX segmentation model with ONNX Runtime Web (WebAssembly backend).

const LIB_CDN = 'https://cdn.jsdelivr.net/npm/@imgly/[email protected]';

async function loadLibrary() {
  const module = await import(LIB_CDN + '/+esm');
  removeBackgroundFn = module.removeBackground;
}

The first call downloads ~40MB of model weights. That sounds heavy, but:

  • The browser caches it automatically
  • Subsequent uses load instantly from cache
  • No server round-trip on any future use

This is the same trade-off FFmpeg.wasm makes — big initial download, but then your browser becomes a local processing powerhouse.

Stage 2: Running AI Inference Locally

Once the model is loaded, inference is straightforward:

const imageBlob = await new Promise(r => canvas.toBlob(r, 'image/png'));

const resultBlob = await removeBackgroundFn(imageBlob, {
  model: 'medium',
  output: { format: 'image/png' },
  progress: (key, current, total) => {
    // Update loading UI
  }
});

What’s happening behind the scenes:

  1. The library resizes your image to the model’s input dimensions
  2. Pixel data is converted to a tensor
  3. ONNX Runtime Web runs the segmentation model via WebAssembly
  4. The output tensor (a per-pixel foreground probability map) is converted back to an image with transparent background

The medium model balances quality and speed. On a decent laptop, inference takes 2-5 seconds for a typical photo. On a phone, maybe 8-15 seconds. Acceptable for a free, private tool.

Stage 3: Building the Editable Mask

Here’s where it gets interesting. The AI output isn’t final — it’s a starting point. I extract the alpha channel from the AI result and build an editable grayscale mask:

async function buildMaskFromResult() {
  const w = originalImage.naturalWidth;
  const h = originalImage.naturalHeight;

  // Draw AI result to a temporary canvas
  const resultCanvas = document.createElement('canvas');
  resultCanvas.width = w;
  resultCanvas.height = h;
  const rCtx = resultCanvas.getContext('2d');
  rCtx.drawImage(resultImg, 0, 0);
  const resultData = rCtx.getImageData(0, 0, w, h);

  // Extract alpha channel → grayscale mask
  // White = foreground (keep), Black = background (remove)
  maskCanvas = document.createElement('canvas');
  maskCanvas.width = w;
  maskCanvas.height = h;
  maskCtx = maskCanvas.getContext('2d');
  const maskData = maskCtx.createImageData(w, h);

  for (let i = 0; i < resultData.data.length; i += 4) {
    const alpha = resultData.data[i + 3];
    maskData.data[i] = alpha;     // R
    maskData.data[i + 1] = alpha; // G
    maskData.data[i + 2] = alpha; // B
    maskData.data[i + 3] = 255;   // A (mask itself is always opaque)
  }
  maskCtx.putImageData(maskData, 0, 0);
}

Why a separate mask canvas?

Because users need to fix the AI’s mistakes. Hair edges, transparent objects, similar-colored backgrounds — no AI gets these perfect 100% of the time. The mask canvas becomes a paintable surface.

Stage 4: Manual Refinement with Brush & Eraser

This is the feature that separates a toy demo from a usable tool. Users can:

  • Brush (paint white on mask) → restore foreground areas the AI removed
  • Eraser (paint black on mask) → remove background areas the AI missed
function paintOnMask(e) {
  const rect = editCanvas.getBoundingClientRect();
  const x = (e.clientX - rect.left) / rect.width * maskCanvas.width;
  const y = (e.clientY - rect.top) / rect.height * maskCanvas.height;

  const brushSize = parseInt(brushSizeEl.value);
  const softness = parseInt(brushSoftEl.value) / 100;

  maskCtx.lineCap = 'round';
  maskCtx.lineWidth = brushSize;

  // Softness = CSS filter blur on the mask canvas context
  if (softness > 0) {
    maskCtx.filter = `blur(${Math.round(brushSize * softness * 0.3)}px)`;
  }

  if (currentTool === 'brush') {
    maskCtx.globalCompositeOperation = 'lighter';
    maskCtx.strokeStyle = '#ffffff';
  } else {
    maskCtx.globalCompositeOperation = 'source-over';
    maskCtx.strokeStyle = '#000000';
  }

  maskCtx.beginPath();
  maskCtx.moveTo(lastX, lastY);
  maskCtx.lineTo(x, y);
  maskCtx.stroke();
}

Key details:

  • Coordinate mapping: The edit canvas is CSS-scaled to fit the viewport, but the mask operates at full image resolution. Every mouse position gets mapped from display coordinates to mask coordinates.
  • Edge softness: Uses Canvas 2D filter: blur() on the stroke — this creates feathered edges instead of hard cuts.
  • Undo stack: Each mousedown saves a full ImageData snapshot of the mask. Up to 20 undo levels.

The brush cursor is a position: fixed div that follows the mouse, sized to match the display-scaled brush diameter. The actual canvas cursor is set to none.

Stage 5: Compositing the Final Output

To generate the downloadable PNG, the mask is applied to the original image:

function applyMaskToOriginal() {
  const origData = origCtx.getImageData(0, 0, w, h);
  const mData = maskCtx.getImageData(0, 0, w, h);
  const outData = oCtx.createImageData(w, h);

  for (let i = 0; i < origData.data.length; i += 4) {
    outData.data[i] = origData.data[i];       // R — original
    outData.data[i + 1] = origData.data[i + 1]; // G — original
    outData.data[i + 2] = origData.data[i + 2]; // B — original
    outData.data[i + 3] = mData.data[i];       // A — from mask R channel
  }

  oCtx.putImageData(outData, 0, 0);
  return outCanvas;
}

The mask’s R channel (which equals G and B since it’s grayscale) becomes the alpha channel of the output. White mask pixels → fully opaque. Black → fully transparent. Gray → semi-transparent (useful for hair and soft edges).

The Refine Mode Overlay

In refine mode, users see the original image with a semi-transparent red overlay on removed areas:

function renderMaskOverlay() {
  editCtx.drawImage(maskCanvas, 0, 0, dw, dh);
  const overlayData = editCtx.getImageData(0, 0, dw, dh);

  for (let i = 0; i < overlayData.data.length; i += 4) {
    const maskVal = overlayData.data[i];
    if (maskVal < 128) {
      // Removed area → semi-transparent red
      overlayData.data[i] = 220;     // R
      overlayData.data[i + 1] = 50;  // G
      overlayData.data[i + 2] = 50;  // B
      overlayData.data[i + 3] = 120; // A
    } else {
      // Kept area → fully transparent (show original underneath)
      overlayData.data[i + 3] = 0;
    }
  }
  editCtx.putImageData(overlayData, 0, 0);
}

This gives immediate visual feedback — you can see exactly what the AI removed and paint corrections in real time.

Performance Considerations

  • Memory: Three full-resolution canvases live in memory (original, mask, output). For a 4000×3000 photo, that’s ~144MB of pixel data. Mobile devices with <4GB RAM may struggle.
  • Real-time rendering: Every brush stroke triggers renderPreview() via requestAnimationFrame. This redraws the preview canvas + overlay from the mask. On large images, there’s a noticeable lag.
  • Touch support: Full touch event handling with passive: false to prevent scroll interference.

What I Stripped for the Open-Source Version

The production version on ToolKnit includes:

  • Daily usage limits (fair-use throttling)
  • Analytics tracking
  • Self-hosted model weights (faster loading from our CDN)
  • Sound effects on completion
  • Site navigation and SEO shell

The open-source version strips all of that down to two files:

  • index.html — standalone UI (~250 lines)
  • app.js — core logic (~380 lines)

You can clone it, run npx serve ., and have a working background remover in 30 seconds.

What’s Next

Some ideas for anyone who wants to fork and extend:

  • Background replacement — solid color or custom image behind the subject
  • Batch processing — drop multiple images, process all sequentially
  • WebGPU acceleration — ONNX Runtime Web supports WebGPU; inference could be 3-5x faster
  • Edge feathering controls — post-process the mask with adjustable blur radius
  • Before/after slider — drag to compare original and result

Try It

If you’ve ever needed to remove a background without uploading your photo to a random website — this is it. Clone it, use it, break it, improve it.

Built by Zihang Dong. Building browser-first tools at ToolKnit.