KotlinConf’26 Keynote Highlights: Advances in Language Design, Tooling, AI-Driven Workflows, and Multiplatform Development

Kotlin turns 15 this year, and it really is everywhere. It powers systems behind everyday moments, such as tapping to pay, buying commuter rail tickets, using in-flight entertainment, and even filing tax returns online. As AI continues to reshape how software gets built, Kotlin’s growing real-world impact reflects the importance of languages and tools that help teams manage complexity, express ideas clearly, and build reliable systems with confidence.

At KotlinConf’26, the JetBrains team and industry partners shared how Kotlin continues to evolve for developers at every scale. The keynote highlighted advances in language design, tooling, AI-driven workflows, and multiplatform development – all aimed at improving the Kotlin development experience for building modern applications everywhere.

KotlinConf is in full swing. Join us online to watch the livestream!

Evolving Kotlin

As AI-driven development raises the level of abstraction, trust in the programming language is more important than ever. Kotlin Lead Language Designer Michail Zarečenskij mentioned that with Kotlin, the team aims to provide that trust at every level. Ergonomics and safety are principles that guide the language at its very core.

Michail previewed Kotlin 2.4.0 – the next step in Kotlin’s evolution toward safer and more ergonomic code. Among the features being stabilized are context parameters, designed to make APIs more expressive and focused on core logic, and explicit backing fields, which simplify common backing property patterns while reducing boilerplate and improving safety.

The presentation also covered several experimental language features, including multi-field value classes for modeling domain-specific data such as money or colors. Key aspects of value classes include:

  • The compiler automatically generates functions like equals(), hashCode(), and toString().
  • Value classes use safer name-based destructuring by default.
  • Value classes have no identity semantics – they are fully defined by their properties.
Kotlin value classes

These changes are designed to make working with data safer, more expressive, and more efficient over time.

Among other experimental features, the presentation highlighted updates to collection literals, locality as a first-class language concept, and rich errors, a new approach to representing and handling recoverable failures.

Kotlin 2.4.0 Preview

Kotlin ecosystem

Tooling has been part of Kotlin’s story from the beginning. As Kotlin expands into new workflows, including agents and integrations, the ecosystem continues to apply the same core principles of ergonomics and safety. The goal is to ensure a consistent development experience with any editor, build tool, or agentic framework.

One of the major announcements was the Kotlin Toolchain – a unified entry point into the Kotlin ecosystem. Available through a single command, Kotlin Toolchain brings together everything from creating, building, running, and testing applications to formatting code, generating documentation, and integrating with agents.

Kotlin Toolchain

Starting today, you can already use Kootlin Toolchain in your JVM and multiplatform projects to build, run, and test your apps, with Amper now serving as the core part of the Kotlin Toolchain. In the future, Kotlin Toolchain will expand with LSP integrations, AI skills, native dependency provisioning, and much more. As always, JetBrains is also bringing deep IDE integrations for the best possible out-of-the-box experience.

The presentation also introduced the Kotlin Documentation Model, a core part of Kotlin that represents machine-readable documentation in the form of a kdoc.jar. This specified, backward-compatible format will be published alongside libraries and consumed by IDEs, web tools like Dokka, and AI agents.

Another major announcement was the promotion of the Kotlin Language Server to Alpha. Backed by the full power of the IntelliJ engine, LSP provides a more consistent experience across diagnostics, code completion, and tooling support. The official Kotlin extension for Visual Studio Code is now also available on the Visual Studio Marketplace.

Kotlin Language Server (Alpha)

Learn more

As part of Kotlin Foundation’s efforts, JetBrains and Meta have started the process of standardizing ktfmt and making it a core part of Kotlin.

The team also announced ongoing collaboration with the open-source community to bring first-class Kotlin support to official Bazel rules_kotlin, making it easier to use Kotlin in large-scale codebases with thousands of modules.

Kotlin at Google

Google has been using Kotlin in production for over a decade, and 92% of professional Android developers now use Kotlin for Android applications.

92% of professional Android developers now use Kotlin for Android applications

The keynote also highlighted Google’s ongoing collaboration with JetBrains on the K2 compiler. Since launching stable K2 support in Android Studio, the Google team has seen near-universal adoption. In Kotlin Symbol Processing, Kotlin’s solution to Java annotation processes, which Google built and maintains, a 17% reduction in execution time for complex builds was achieved. In R8, Android’s whole-program optimization tool, the team rewrote coroutine locks to avoid reflection, saving up to 50% on composed performance benchmarks.

AI tooling for Kotlin

The keynote also focused on the next generation of AI tools for Kotlin development. We want you to be able to use any agent directly inside JetBrains IDEs. To support these efforts, JetBrains is co-leading the development of an open standard, the Agent Client Protocol (ACP), which specifies how IDEs and coding agents communicate. You can read more about it in our dedicated blog post: Our 2026 Direction: AI and Classic Workflows in JetBrains IDEs.

Agent Client Protocol (ACP), which specifies how IDEs and coding agents communicate

Junie

Junie, JetBrains’ coding agent, is deeply integrated with JetBrains IDEs, and even the Junie CLI version can connect to the IDE to get full project context. Junie also works with different LLM providers, allowing you to choose the best model for a specific task. While Junie already works in Kotlin projects, it now also includes dedicated Android support. 

Junie, JetBrains’ coding agent, deeply integrated with JetBrains IDEs

JetBrains Air

As developers are becoming more productive with agents, the keynote also explored how to scale agent-based development workflows. JetBrains Air is an agentic development environment for working effectively with multiple agents.

JetBrains Air, an agentic development environment for working effectively with multiple agents

OpenAI Codex, Claude Agent, Gemini CLI, and Junie can execute independent task loops without conflicting with one another. You can start agents in separate Git worktrees or Docker containers, and to share progress with your entire team, you’ll soon be able to use cloud agents and even start and guide them directly from the browser.

JetBrains Air: OpenAI Codex, Claude Agent, Gemini CLI, and Junie can execute independent task loops without conflicting with one another

Anthropic and JetBrains

Christian Ryan, who leads applied AI engineering at Anthropic in Europe, joined the keynote to highlight the growing collaboration between Anthropic and JetBrains across AI tooling, libraries, and developer workflows. When Anthropic built their official JVM SDK, they used Kotlin, which allowed them to create the SDK in an ergonomic, concise, null-safe language. The collaboration also includes the official Kotlin MCP SDK.

On the tooling side, Claude is now native in IntelliJ IDEA and Android Studio. Claude is also a first-party model in Junie and JetBrains Air. For CLI users, there’s a plugin for Claude Code that integrates JetBrains’ official Kotlin LSP for deeper project understanding.

Anthropic and JetBrains: a deep partnership

A new Kotlin SWE-bench based on 110 real engineering tasks from Kotlin repositories was introduced during the keynote. Using identical prompts and agent configurations, Claude Code with Opus 4.7 achieved the highest resolution rate at 86.4%.

Claude Code: Kotlin SWE-Bench

Koog 1.0

Vadim Briliantov, Technical Lead and author of Koog, continued the keynote with a talk about the Kotlin AI agent framework that allows you to build fault-tolerant, scalable, and enterprise-ready AI agents in fully idiomatic Kotlin. Vadim announced the stable release of Koog 1.0, a major milestone for production-ready agent development in Kotlin across backend, mobile, and multiplatform applications.

Koog 1.0

The presentation focused on Koog’s approach to building reliable AI systems through type-safe workflow DSLs, persistence and recovery for long-running agents, and deep integrations with existing Kotlin ecosystems, including Spring AI, Ktor, and observability tooling. One of the featured case studies came from Mercedes-Benz, whose team uses Koog to build vehicle maintenance support agents with structured workflows and carefully controlled execution logic. Read the full case study.

Koog case study: Mercedes-Benz

Vadim also showcased multiplatform support and on-device AI capabilities on Android using Google’s Gemma models, reinforcing Kotlin’s growing position as a unified language for building modern AI-powered applications – from backend services to mobile experiences – all in Kotlin.

View on GitHub

Kotlin for backend development

The keynote continued with updates on Kotlin for backend development, including new capabilities across Ktor, kotlinx-rpc, and Exposed. The team showcased Koog integration for building AI-powered services with Ktor, experimental first-party gRPC support in kotlinx-rpc, and the stable release of Exposed, which introduces vector types for AI-powered similarity search alongside a new Gradle plugin for simplified migration script generation. A new agent skill is also available to help developers migrate existing projects to Exposed 1.0.

Beyond tooling, the presentation focused on Kotlin’s growing adoption in enterprise and compliance-driven environments, where reliability and long-term support are critical.

Adoption of Kotlin for backend development

Starting with Kotlin 2.4, the Kotlin standard library will include an 18-month security support policy, with security fixes backported to all release lines within an active support window.

Explore more

The keynote also highlighted Kotlin’s productivity benefits for backend teams, referencing data showing 15–20% faster development cycles as projects grow in complexity. 

Kotlin’s productivity benefits for backend teams: 15–20% faster development cycles

The presentation emphasized Kotlin’s deep integration with the JVM ecosystem through ongoing collaboration with Spring, improved Kotlin representation in Spring and JUnit documentation, updates to the kotlin-maven-plugin and Maven onboarding experience, improved coroutine support in Micrometer, and continued stabilization of the Lombok compiler plugin for mixed Kotlin-Java projects.

Kotlin Multiplatform

Kotlin Multiplatform continues to see rapid adoption, with the number of top apps using KMP more than doubling over the past year. Companies such as PayPal, Booking.com, Sony, and Duolingo are already using it in production, and more teams are adopting Compose Multiplatform to share UI across platforms. 

For example, Sony uses KMP in their Sound Connect app for headphones to work with platform APIs like sensors and background processing while sharing the UI through Compose Multiplatform. Across Kotlin Multiplatform case studies overall, applications built with KMP now serve hundreds of millions of users daily.

Kotlin Multiplatform (KMP) adoption in companies

Getting started with KMP is now easier with the KMP IDE plugin available on all operating systems for both IntelliJ IDEA and Android Studio. The plugin offers everything you need to build great KMP apps with convenient run configurations, tools for working with Compose code, integrations with Swift and cross-language features, and AGP 9.0 support.

You can also create new projects right in the IDE with the KMP project wizard, which now uses our new default structure, where each module has a single clear responsibility.

We are working to enhance the iOS development experience, notably through Swift Export features that make calling Kotlin from Swift more natural. In Kotlin 2.4, Swift Export is officially moving to Alpha. We also introduced SPM import, which lets you add dependencies on Objective-C-compatible code using Swift Package Manager and call into those APIs directly from Kotlin code.

Kotlin/Native has seen significant performance improvements over the last year. Measured on the Google Docs codebase, build times are now 25% faster while using less than half the RAM during builds compared to a year ago.

Kotlin/Native performance from Kotlin 2.2 to 2.4

Compose Multiplatform

Compose Multiplatform is fully stable and production-ready on mobile and desktop. The web platform also reached Beta status in September 2025, marking another major step forward for multiplatform UI development. For all these platforms, the team continues to bring you the latest improvements and APIs from Jetpack Compose. One of the biggest highlights over the past year is the new Navigation 3 library, a flexible, Compose-first solution that gives you full control over your back stack – and it’s already stable for multiplatform use.

On iOS, new interop APIs now make it possible to combine native Liquid Glass components with Compose UI, allowing native views to dynamically interact with Compose content underneath.

Beyond the framework itself, the Kotlin Multiplatform ecosystem continues to expand rapidly. There are now more than 3,500 community libraries listed on klibs.io, giving you a growing set of tools and integrations for building multiplatform applications across mobile, desktop, backend, and web.

The growing Kotlin Multiplatform (KMP) ecosystem. Libraries indexed by klibs.io

Conclusion 

KotlinConf’26 highlighted how Kotlin continues to evolve beyond a programming language into a complete ecosystem for backend, mobile, web, AI, and multiplatform development. From language and tooling improvements to growing industry adoption, the announcements reflected the shared goal of helping developers build modern software with greater clarity, safety, and productivity.

Watch the livestream

TeamCity 2025.11.5 Is Out

Our (most likely) final update for TeamCity 2025.11 On-Premises servers has just been released. This updage addresses a tiny amount of issues, but includes four security problem fixes, so we recommend that you do not skip this update.

See TeamCity 2025.11.5 Release Notes for the complete list of resolved issues.

Why update?

Staying up to date with minor releases ensures your TeamCity instance benefits from the following:

  • Performance improvements.
  • Better compatibility with integrations.
  • Faster, more stable builds.
  • Enhanced security for your workflows.

Compatibility

TeamCity 2025.11.5 shares the same data format as all 2025.11.x releases. You can upgrade or downgrade within this series without the need for backup and restoration.

How to upgrade

  1. Use the automatic update feature in your current TeamCity version.
  2. Download the latest version directly from the JetBrains website.
  3. Pull the updated TeamCity Docker image.

Need help?

Thank you for reporting issues and providing feedback! If you have questions or run into any problems, please let us know via the TeamCity Forum or Issue Tracker.

Happy building!

Improving Accessibility in JetBrains IDEs: What’s New and What’s Next in 2026

Making software accessible often comes down to removing small but repeated points of friction in everyday workflows. Today, on Global Accessibility Awareness Day, we’re sharing recent improvements in JetBrains IDEs across several areas: compatibility with assistive technologies on various platforms, keyboard navigation, and non-visual feedback. Some of these improvements are already available, and some are coming later this year.

You can use the audio player below to listen to this blog post.

Better compatibility with assistive technologies

One of the key areas we’ve been working on is improving how JetBrains IDEs interact with OS-level accessibility tools.

Improved Magnifier support on Windows

Screen magnifiers are among the most commonly used assistive technologies in JetBrains IDEs. Until recently, the built-in Windows Magnifier didn’t reliably follow the text cursor in the editor, making navigation and editing more difficult for low-vision users. We’ve implemented support for cursor tracking so Magnifier follows text as you type, just as it does in other applications.

This builds on earlier work on macOS, where we addressed text cursor tracking with macOS Zoom. Now, the same support is being extended to Windows.

Orca and GNOME Magnifier support on Linux

With version 2026.2, coming this summer, JetBrains IDEs will allow you to use the Orca screen reader and GNOME Magnifier in supported Linux environments. 

This is an active area of work, with multiple related tasks already underway. Accessibility shouldn’t depend on your operating system, and we’re continuing to improve support across platforms.

More predictable keyboard navigation

We’ve also been making it easier to move through the IDE without relying on a mouse.

Main menu access with Alt on Windows

In native Windows applications, pressing Alt moves the focus to the main menu, allowing you to navigate it with the keyboard. This behavior was previously missing from JetBrains IDEs, and screen readers, such as NVDA, would sometimes announce the system menu instead.

Now, the main menu behaves in a way that feels familiar and predictable for keyboard-only and screen-reader users, and the bright focus indicator helps low-vision users identify the selected item.  

Navigating between major parts of the IDE

Another focus area is the experience of moving between different parts of the IDE interface, such as toolbars, panels, and the editor. We’re working on a more structured model for navigating through the big component groups:

  • Tab and Shift+Tab move the focus within the current area.
  • A dedicated shortcut lets you jump between larger sections of the IDE.

This reduces the effort required to reach essential controls and makes the overall layout easier to navigate. For the current iteration, we made it possible to bring the main toolbar and status bar into focus, and we fixed the Project and Git toolbar widgets, which were not selectable by screen readers, even though other elements already were. 

As the next step, we’ll polish specific controls and include tool window bars on both sides of the IDE frame in the navigation flow.

Exploring richer non-visual feedback with audio cues

Accessibility is not only about reaching controls, but also about understanding what’s happening while you work. We’re exploring ways to provide richer audio feedback in the IDE. Two directions we’re currently investigating:

  • Contextual signals when the caret lands on lines with errors, warnings, breakpoints, or version control changes. We want the IDE to provide immediate, non-visual feedback in context.
  • More general audio notifications for IDE actions and state changes.

The goal is to reduce the need to rely on visual indicators or switch contexts just to understand what changed. Instead, we want the IDE to provide that information more directly.

Accessibility as an ongoing effort

We’re improving accessibility in JetBrains IDEs across multiple areas at once, including by providing compatibility with assistive technologies like screen readers and magnifiers, as well as by offering more consistent keyboard navigation and clearer feedback for events that are otherwise mostly visual.

These improvements build on earlier updates, such as support for VoiceOver and NVDA, a high-contrast UI theme, and color schemes for red-green vision deficiency. There’s still more to do, and we’ll continue working in this direction.

We’d love to hear from you

We’re eager to hear from developers who rely on accessibility features, as well as from anyone interested in improving the experience of using them.

If you have ideas or feedback about accessibility in JetBrains IDEs, you can reach us directly at accessibility@jetbrains.com. You can also report issues through YouTrack or the support request form.

If you’d like to stay informed about accessibility improvements, you can subscribe to updates here.

Official Kotlin Support for Visual Studio Code Is Now Available in Alpha

Today at KotlinConf 2026, we announced the Alpha release of the official Kotlin extension for Visual Studio Code.

IntelliJ IDEA and Android Studio remain the most complete environments for Kotlin development. But not every Kotlin developer works there every day. Some prefer VS Code, or already have it as part of their setup. Kotlin by JetBrains is an extension for those developers: official Kotlin language support in VS Code, powered by the Kotlin Language Server, with core features for reading, writing, and navigating Kotlin code.

Install Kotlin by JetBrains for VS Code

What you get

The Kotlin by JetBrains extension connects VS Code to the Kotlin Language Server built on IntelliJ IDEA’s code-insight infrastructure and the Kotlin plugin implementation.

In practice, this means Kotlin-aware features in VS Code are backed by the same foundation we use for Kotlin support in IntelliJ IDEA. The Alpha release includes core editor support such as code completion, diagnostics, navigation, quick fixes, formatting, and project import.

For the full list of supported features, see the Kotlin by JetBrains extension page on the Visual Studio Marketplace.

To get started, install the extension from the Visual Studio Marketplace, open a Kotlin project, and start coding.

Part of a broader Kotlin tooling effort

This release is part of our ongoing work to support Kotlin developers across different tools and workflows.

Earlier this year, we introduced the Java to Kotlin Converter extension for VS Code, which helps developers convert Java files to Kotlin without leaving their editor. Kotlin by JetBrains continues that effort by bringing language-aware Kotlin editing support to VS Code.

Developers using other editors that support the Language Server Protocol can also try the Kotlin Language Server, but setup is manual and requires an editor with pull-based diagnostics support.

Help shape Kotlin support in VS Code

This is an Alpha release, so your feedback is especially important at this stage. Try it in your own projects and let us know what works well, what breaks, and what’s missing from your workflow. 

Please report any issues or share your feedback in our GitHub repository.

Install Kotlin by JetBrains for VS Code

How I use WP-CLI to cut WordPress maintenance time from 6 hours to 20 minutes

Before WP-CLI, WordPress maintenance meant: browser open, log in to each site, click Update All, check if anything broke, log out, repeat. With 8 clients that was a half-day of clicking.

Now it’s one command per site, or one command for all sites. Here’s exactly how.

What WP-CLI actually is

WP-CLI is the command-line interface for WordPress. Everything you can do in the WordPress dashboard, you can do faster from the terminal — plus things the dashboard can’t do at all.

Install once on your server:

curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
mv wp-cli.phar /usr/local/bin/wp
wp --info

No browser. No clicking. Full WordPress control from SSH.

The basic maintenance commands

Check what needs updating:

wp --path=/var/www/client --allow-root core check-update
wp --path=/var/www/client --allow-root plugin list --update=available --format=table
wp --path=/var/www/client --allow-root theme list --update=available --format=table

Run all updates:

wp --path=/var/www/client --allow-root core update
wp --path=/var/www/client --allow-root plugin update --all
wp --path=/var/www/client --allow-root theme update --all

Verify integrity after update:

wp --path=/var/www/client --allow-root core verify-checksums
wp --path=/var/www/client --allow-root plugin verify-checksums --all

The backup-first pattern

Never update without a backup. Two commands:

# Database backup
wp --path=/var/www/client --allow-root db export backup_$(date +%Y%m%d_%H%M).sql

# Check backup file exists and isn't empty
ls -lh backup_*.sql

If the update breaks something: restore from the backup you just took, investigate what went wrong, try again.

The multi-site loop

This is where the real time savings happen. Store client configs in a JSON file, loop through them all:

#!/bin/bash
# clients.json format:
# [{"name":"client1","path":"/var/www/client1","url":"client1.com"}, ...]

CLIENTS=$(cat clients.json)
ERRORS=0

echo "$CLIENTS" | jq -c '.[]' | while read client; do
    NAME=$(echo "$client" | jq -r '.name')
    PATH=$(echo "$client" | jq -r '.path')
    URL=$(echo "$client" | jq -r '.url')

    echo "=== Processing: $NAME ==="

    # Backup
    wp --path="$PATH" --allow-root db export /backups/${NAME}_$(date +%Y%m%d).sql 2>&1

    # Update
    wp --path="$PATH" --allow-root plugin update --all 2>&1
    wp --path="$PATH" --allow-root core update 2>&1

    # Verify site responds
    STATUS=$(curl -o /dev/null -s -w "%{http_code}" "https://$URL")
    if [ "$STATUS" != "200" ]; then
        echo "WARNING: $URL returned $STATUS after update"
        ERRORS=$((ERRORS+1))
    fi

    echo "=== Done: $NAME ==="
done

echo "Completed. Errors: $ERRORS"

Run this script once. Every client gets backed up and updated. Any site that stops responding after the update triggers a warning.

Security checks in one command

# Check for the common security issues
wp --path=/var/www/client --allow-root user list --role=administrator --fields=ID,user_login,user_email
wp --path=/var/www/client --allow-root core verify-checksums
wp --path=/var/www/client --allow-root doctor check --all

# Check file permissions
stat -c "%a %n" /var/www/client/wp-config.php
find /var/www/client -name "*.php" -perm /o+w 2>/dev/null | head -10

The doctor check --all command requires the WP CLI doctor package:

wp package install wp-cli/doctor-command

It checks PHP errors, inactive plugins, option table bloat, cron job health, and more. One command gives you a full site health report.

Database maintenance

# Check autoloaded data size (should be under 1MB)
wp --path=/var/www/client --allow-root db query 
  "SELECT SUM(LENGTH(option_value))/1024/1024 as mb FROM wp_options WHERE autoload = 'yes';"

# Count post revisions
wp --path=/var/www/client --allow-root db query 
  "SELECT COUNT(*) FROM wp_posts WHERE post_type = 'revision';"

# Optimize database tables
wp --path=/var/www/client --allow-root db optimize

On sites with 2+ years of content, cleaning up revisions and transients can reclaim hundreds of MB and noticeably speed up the admin dashboard.

Cache management

# Flush all caches
wp --path=/var/www/client --allow-root cache flush
wp --path=/var/www/client --allow-root rewrite flush

# LiteSpeed Cache specific
wp --path=/var/www/client --allow-root litespeed-purge all

# W3 Total Cache
wp --path=/var/www/client --allow-root w3-total-cache flush all

Always flush caches after updates. A cached version of a broken page is harder to diagnose than a broken page.

Generating the client report from WP-CLI output

I pipe WP-CLI output to a log file, then format it into an HTML report:

#!/bin/bash
LOG_FILE="/reports/client1_$(date +%Y%m%d).log"

{
  echo "=== WordPress Maintenance Report: $(date) ==="
  echo ""
  echo "--- PLUGIN UPDATES ---"
  wp --path=/var/www/client1 --allow-root plugin update --all
  echo ""
  echo "--- CORE VERSION ---"
  wp --path=/var/www/client1 --allow-root core version
  echo ""
  echo "--- ADMIN USERS ---"
  wp --path=/var/www/client1 --allow-root user list --role=administrator --fields=user_login,user_email
  echo ""
  echo "--- BACKUP STATUS ---"
  ls -lh /backups/client1_*.sql | tail -3
} > "$LOG_FILE" 2>&1

echo "Report saved: $LOG_FILE"

The log file gets converted to a formatted HTML email and sent to the client automatically. They see professional documentation of everything that happened. You spent 20 minutes running scripts; they received a polished report.

WP-CLI on Windows (PowerShell)

For Windows servers or if you’re running maintenance scripts from Windows:

# WP-CLI works via SSH from PowerShell
$session = New-SSHSession -ComputerName "clientserver.com" -Credential $cred
Invoke-SSHCommand -SessionId $session.SessionId -Command "wp --path=/var/www/client --allow-root plugin update --all"

Or use the PowerShell equivalent script that SSH’s into each client server and runs the maintenance commands remotely.

The time comparison

Manual (browser-based):

  • Log in to each WP dashboard: 2 min/site
  • Check for updates: 1 min/site
  • Run updates: 3-5 min/site (waiting for each plugin)
  • Verify site still works: 2 min/site
  • Write update notes: 3 min/site
  • Total: ~11 min/site x 8 sites = ~90 minutes

WP-CLI automated:

  • Start the script: 1 min
  • Wait for it to run: 15-20 min (unattended)
  • Review the output log: 3 min
  • Total: ~20-25 minutes for all 8 sites

The script runs while you do other work. You review the output when it’s done.

The full script bundle (Bash for Linux, PowerShell for Windows, clients.json template, HTML report generator):
WordPress Agency Automation Bundle — skip the 12 hours of building it yourself.

Related articles

  • I automated WP maintenance across 8 client sites
  • WordPress plugin conflicts: diagnose and fix
  • WordPress staging environments: the 15-min setup
  • WordPress security: 10-minute monthly checklist
  • MainWP vs ManageWP vs custom scripts

  • WordPress client onboarding: the exact process

  • WordPress speed optimization: 6 fixes that actually work

All paid tools: devautomation.gumroad.com

What’s your most-used WP-CLI command? Drop it in the comments.

Templating got me to 33,620 pages. Indexing them was the hard part.

I had a fantasy.

The fantasy was that programmatic SEO would let me skip the part where you write 200 individual articles and trick Google into ranking them. I had a structured dataset (US ZIP codes, cities, states) and a templated page generator. So I shipped 33,620 pages overnight. Then I sat back and waited for the long-tail traffic to roll in.

That’s not what happened.

A few months later, I have a more nuanced view. Here are the five lessons I would have paid actual money for at the start.

The numbers, so we’re working from data

Out of 33,620 templated pages on my site:

  • ~6,200 are indexed
  • ~18,000 are in “Crawled, currently not indexed” (CCNI)
  • ~8,500 are “Discovered, currently not indexed”
  • ~700 are flagged as soft 404
  • The rest are recently submitted and still in queue

That’s an indexation rate of about 18%. Sounds low. It kind of is. But here’s the catch: the 6,200 indexed pages drive the vast majority of organic traffic. The CCNI pile is mostly low-traffic ZIPs in low-population areas. Google has decided those aren’t worth indexing, and honestly they’re probably right.

If you came into programmatic SEO expecting “ship N pages, get N pages of traffic,” recalibrate. The real math is closer to “ship N pages, get 15-25% indexed, and the indexed pile drives 95% of your traffic.”

Lesson 1: Templating gives you the structure, not the content

This is the biggest one and the one most programmatic SEO tutorials skip.

My page generator spit out pages with this rough structure:

  1. Hero (city, state, ZIP)
  2. Search box
  3. List of nearby gas stations
  4. State average price comparison
  5. “How to save on gas in {city}” section
  6. Footer

Sounds reasonable. The problem: when I put two pages side by side (one for ZIPs in California, one for ZIPs in Maine), they were 95% identical text. The hero changed. The station list was different. Everything else was the same paragraph with one word swapped.

Google’s templated-content detection is real and it’s specifically looking for this. There’s a metric (referenced in their public ranking patents as “boilerplate ratio”) that measures what fraction of a page is shared with other pages on the same site. High boilerplate ratio plus thin unique content equals a soft 404 verdict.

The fix was to make the templated content actually vary. I built a state-context module with hand-written paragraphs for the 15 highest-traffic states. Each is 80 to 120 words covering the state’s gas tax, refinery capacity, regulatory regime, and seasonal pricing patterns. The other 35 states got a parameterized template with state-specific variables (avg price, neighboring states, gas tax rate). After this shipped, indexation rates climbed about 15% over six weeks.

The 80/20 here: hand-writing the top 15 of 50 states covered about 75% of my traffic. The other 35 got the templated treatment, which is fine because they’re not driving meaningful traffic anyway.

If I’d known this at the start, I would have spent the first weekend writing 15 paragraphs by hand and the second weekend generating the rest. Instead I spent four weekends generating everything and then went back to hand-write the top tier. Same end state, slower path.

Lesson 2: Internal URL types compete with each other

I had two URL types covering the same entity. A programmatic city page (/houston-tx) and a blog post about Houston gas prices (/blog/cheapest-gas-houston). Different formats, different content, both about Houston gas.

Google saw these as competitors. It picked one to index (the blog post) and put the other in CCNI. Then it picked the other for some sister cities (city page won, blog post in CCNI). The pattern was inconsistent and frustrating.

My first instinct was wrong. I tried to “fix” this by submitting the CCNI page for re-validation, hoping Google would just index both. That’s not how this works. Google has a per-site indexation budget and a canonical-decision system that picks one URL per topic per intent. When you ask it to re-evaluate, it just makes the same decision again.

The actual fix was to differentiate the intent of each URL type:

  • Blog post: opinionated guide, current events, “why are gas prices high in Houston this month”
  • City page: factual reference, station list, ZIP grid, state context, “where to find cheap gas in Houston”

Once the content categories were clearly different, Google’s canonical decisions became consistent. Blog posts started ranking for query-with-intent searches (“why are gas prices high in Houston”). City pages ranked for quantity-intent searches (“cheap gas Houston”). They stopped competing for the same impressions.

If you have two URL types covering similar entities, they’re probably competing whether you intended it or not. Audit your URLs and make sure each type has a clear, different job.

Lesson 3: Word count is a proxy, not a target

I spent some time obsessing over word counts. I’d seen “1,500 word minimum for ranking” quoted in SEO guides. So I padded pages with filler to hit the number.

Google did not care. The pages that broke through to indexed weren’t the ones with 1,500 words. They were the ones with 800 words of actually useful, page-specific content.

Word count is a proxy for depth. If you can get to 800 words by writing things that are genuinely useful and page-specific, you’re done. If you’re padding to hit a target, Google can tell, and the padding doesn’t help. The detection isn’t a single signal, it’s a combination: sentence-level perplexity, semantic distinctness from other pages on your site, contextual relevance to the query intent. Modern ranking models are sophisticated enough to recognize stuffing.

I went from a 640-word baseline to an 810-word “after” state by adding things that were genuinely useful: the per-state context, an SSR-rendered nearby ZIP grid, a top-stations summary. None of it was filler. The +170 words mattered because of what was in them, not because of the count.

Lesson 4: GSC verdicts have memory

If a page gets flagged as soft 404, fixing the page doesn’t immediately clear the verdict. The recovery flow is:

  1. Fix the page
  2. Submit a re-validation request in GSC
  3. Wait 1 to 4 weeks for the re-crawl
  4. Accept that some URLs won’t come back even after you fix them

GSC’s verdict is sticky. It’s not because Google is malicious. It’s because the indexation queue is finite and pages with prior negative verdicts get deprioritized. If a page has been “soft 404” for three months and you fix it, Google doesn’t drop everything to re-evaluate. It works through the queue, and your fixed page joins the back.

The implication: when planning a programmatic SEO site, get the page quality right before you submit URLs to GSC. Submitting a thin page early creates a verdict you’ll fight for months. Better to wait, polish, and submit clean.

I did the opposite. I submitted everything immediately because I was excited. About 700 pages got soft-404’d, and clearing those verdicts took about 8 weeks of patient post-mortem work even after the underlying content was fixed.

Lesson 5: Branded search beats backlinks for indexation

This one surprised me.

Pages that started getting branded search impressions (users typing “gas price check Houston” into Google) got indexed faster than pages that had inbound backlinks from other domains. The branded queries didn’t even have to convert. Just the impressions seemed to register.

Google’s signal here seems to be: “people are looking for this specific page on this site, by name.” That signal is hard to fake (it requires real users typing branded queries) so Google trusts it heavily. A backlink can be paid for or manufactured. Branded search volume cannot.

The implication: if you want to accelerate indexation on a specific page, drive direct traffic to it from anywhere. Social posts, email, bio links, even paid ads briefly. The traffic sends a “this page is real and people want it” signal that Google’s ranking model picks up.

I tested this informally. I posted a link to one of my CCNI city pages on Reddit. Got about 80 visits in a day. Within two weeks the page moved from CCNI to indexed, with a top-50 ranking on its target query. I didn’t change the page at all. Just sent traffic.

I’m not claiming this works every time. But the pattern was strong enough that I now bias toward “drive traffic to anything stuck in CCNI” over “build more backlinks to anything stuck in CCNI.”

What I’d do differently

If I were starting over:

  1. Start with 50 hand-written pages, not 50,000 templated ones. Get those indexed and ranking before generating the long tail. The long tail benefits from established authority.
  2. Differentiate URL types from day one. Decide what each URL type is for and don’t blur the line.
  3. Pre-compute everything. If you’re going to template at scale, all unique-per-page content (geocoding, internal links, neighbor lists) should be resolved at build time, not runtime. Runtime fetches show up as “loading…” placeholders in the SSR HTML, and Google reads those as thin content.
  4. Check GSC weekly, not monthly. Soft 404 verdicts compound silently. By the time you have 700 of them, recovery is a multi-month project.
  5. Save the URL submissions for last. Polish first. Submit when the page is actually good. Once a verdict lands, it’s sticky.

The bottom line

Programmatic SEO works. But the math isn’t “ship 33,620 pages, get 33,620 pages of traffic.” It’s closer to “ship 33,620 pages, get 6,200 indexed, get traffic on the top 2,000 of those, and the rest are infrastructure.” The work isn’t generating the pages. The work is earning the right to keep them indexed.

If you’ve shipped a programmatic site at scale, what’s your indexation rate? I’d love to hear what others are seeing. My hypothesis is that 15-25% indexation is typical for first-year programmatic sites, and the rate climbs over time as the site builds authority. But I have a sample size of one, so I’d love more data points.

WordPress staging environments: the 15-minute setup that prevents client emergencies

Every WordPress emergency I’ve seen in the last five years had the same root cause: someone tested an update on production.

A staging environment eliminates this. If it breaks on staging, you fix it on staging. Nothing reaches the client’s live site until it’s verified. Here’s how I set one up in 15 minutes.

Why staging isn’t optional for client sites

“It worked on my machine” is a joke. “It worked on the live site yesterday” is a client emergency.

The risks of updating production directly:

  • Plugin update breaks the theme — client’s site is down during business hours
  • WooCommerce update changes checkout behavior — orders fail, client loses revenue
  • PHP version bump reveals a deprecated function — white screen on every page
  • Theme update wipes custom CSS — site looks wrong until you remember what you changed

A staging environment catches all of these before they matter.

Option 1: Staging via hosting panel (easiest)

Most quality hosts (SiteGround, Kinsta, WP Engine, Cloudways) have one-click staging built in.

SiteGround: Site Tools -> WordPress -> Staging -> Create Staging Copy
Kinsta: MyKinsta -> Site -> Environments -> Add Environment
WP Engine: User Portal -> Sites -> Staging -> Copy to Staging

What this does: creates a full copy of the site (files + database) at a subdomain (usually staging.clientdomain.com or clientdomain.wpengine.com). Changes stay isolated until you push to live.

After testing updates:

  1. Test on staging: update plugins, verify site works
  2. Push to live: one-click merge from staging to production
  3. Verify on production: check key pages, checkout, contact forms

Time for the whole flow: 20-30 minutes. Time for a manual plugin update without staging: 5 minutes. Time to fix a broken production site without staging: 2-6 hours.

Option 2: Local by Flywheel (free, for local testing)

Local (getlocal.io) runs a full WordPress environment on your machine. Free, works offline, fast.

1. Download Local from localwp.com
2. Create new site -> WordPress
3. Click "Pull from host" (connects to WP Engine, Kinsta, etc.)
   Or use WP-CLI to import manually

For clients on shared hosting without staging: this is the fallback. Pull the site locally, test, push changes manually.

Limitation: you can’t test live payment processing locally. Use Option 1 for WooCommerce sites.

Option 3: WP-CLI staging on a VPS (most control)

If you have a VPS or the client’s server has enough space:

#!/bin/bash
LIVE_PATH="/var/www/html/production"
STAGING_PATH="/var/www/html/staging"
STAGING_DOMAIN="staging.clientdomain.com"

# Copy files
cp -r $LIVE_PATH $STAGING_PATH

# Export live database
wp --path=$LIVE_PATH --allow-root db export /tmp/live_backup.sql

# Import to staging database (must create DB first)
mysql -u root -p staging_db < /tmp/live_backup.sql

# Update staging URLs in database
wp --path=$STAGING_PATH --allow-root search-replace "clientdomain.com" "$STAGING_DOMAIN"

# Update wp-config.php for staging database
sed -i "s/production_db/staging_db/" $STAGING_PATH/wp-config.php

Then set up the staging subdomain in nginx/Apache. The staging site is fully isolated.

The staging workflow for monthly maintenance

This is how I handle updates for every client:

1. Create/refresh staging copy (hosting panel, 2 min)
2. Run all updates on staging (WP-CLI or dashboard)
3. Quick verification: homepage, key pages, checkout, contact form
4. If all good: merge to production (1 click)
5. Post-production check: same 4-5 pages

With this workflow, the “update broke the site” emergency essentially doesn’t happen. I’ve been doing this for 3 years across 15+ client sites.

What to test on staging before going live

Minimum verification checklist:

  • [ ] Homepage loads
  • [ ] Main navigation works
  • [ ] A key interior page (About, Services, etc.) loads correctly
  • [ ] Contact form submits (check that email arrives)
  • [ ] Login/logout works
  • [ ] For WooCommerce: add to cart, checkout, payment gateway (test mode)
  • [ ] Admin dashboard accessible
  • [ ] No PHP errors or warnings visible (check debug.log)

This takes 5-10 minutes. It’s the difference between a routine update and an emergency call.

Client communication about staging

I include staging in my service agreements. Clients sometimes push for “just update it live, it’ll be fine” — especially for minor updates.

My response: “Updates on production cost me nothing extra to do safely. If something breaks, it costs both of us time to fix. Staging takes 10 minutes; cleanup takes hours. I always test first.”

Some clients explicitly want to see the staging site before changes go live. I send them the staging URL and a list of what to check. It turns a potential complaint (“why did the button color change?”) into a pre-approved change.

Automating the staging refresh

For sites I update monthly, I automate the staging refresh with a script that runs before the update window:

#!/bin/bash
# Run before monthly maintenance window
LIVE="clientdomain.com"
STAGING="staging.clientdomain.com"

# Refresh staging (Kinsta API - other hosts have similar)
curl -X POST "https://api.kinsta.com/v2/sites/$SITE_ID/environments/$STAGING_ENV_ID/clone-from/$LIVE_ENV_ID" 
  -H "Authorization: Bearer $KINSTA_TOKEN"

echo "Staging refreshed. Ready for updates."

This ensures staging always reflects the current production state, not a 3-month-old copy.

Why most freelancers skip staging (and why that’s the wrong call)

“It takes too long.” — 15 minutes to set up. 5 minutes per update cycle after that.

“The client is on shared hosting with no staging.” — Use Local by Flywheel or ask the client to upgrade to a plan with staging (and explain why).

“It’s just a minor update.” — Plugin conflicts and WooCommerce breaking changes have happened on “minor” updates. The risk profile doesn’t match your definition of minor.

The real cost of skipping staging isn’t the 10 minutes you saved. It’s the 6 hours you spend on an emergency fix, the client relationship damage, and the stress.

The monthly maintenance automation bundle includes pre-update backup scripts that work as a lightweight staging safety net:
WordPress Agency Automation Bundle

Service agreement template that includes staging workflow documentation:
WordPress Agency Starter Kit

Related articles

  • I automated WP maintenance across 8 client sites
  • WordPress plugin conflicts: diagnose and fix
  • WordPress backups: the strategy that actually protects client sites
  • WooCommerce maintenance: 8 checks
  • How to price WordPress maintenance retainers

  • WordPress client onboarding: the exact process

  • WordPress speed optimization: 6 fixes that actually work

All paid tools: devautomation.gumroad.com

What staging setup do you use for client sites — hosting-provided, Local, or something else?

What Is Vibe Coding? And Does It Actually Work for Production Code? (I Tested 10 Tools)

Everyone keeps saying it. Half the people saying it can’t define it. I spent three weeks finding out whether the thing they’re describing actually holds up when you’re building something real.

Let me define vibe coding properly, because the term has been stretched to the point where it means almost anything involving AI and code.

Vibe coding is a development workflow where you describe what you want in natural language, often imprecisely, often iteratively and let an AI tool generate, modify, or explain code based on your intent rather than your specification. The “vibe” is the feeling of directing rather than writing, of being a composer who sketches melodies and lets the AI fill in the notation.

The term was popularised by Andrej Karpathy in early 2025 and it resonated because it named something a lot of developers were already experiencing. You’re not doing traditional programming. You’re not doing no-code. You’re doing something in between, guiding an AI through a problem using natural language plus occasional code review, trusting the tool to handle the implementation details while you stay at the problem level.

The debate is whether this is a legitimate development methodology or a fast path to unmaintainable code that works until it doesn’t.

I tested it on real tasks to find out.

The Testing Methodology

Three task types that cover the range of what developers actually do:

Task 1: Build a React dashboard : A monitoring dashboard with real-time data, filtering and a chart component. Not a toy example, the kind of component you’d actually ship.

Task 2: Debug a Python API : A FastAPI endpoint with a subtle async bug causing intermittent 500 errors under load. The kind of bug that takes a human developer 2-3 hours to find.

Task 3: Refactor legacy code : A 300-line Python function handling multiple concerns simultaneously. The task: split it sensibly without changing behaviour.

Four evaluation dimensions:

  • Code quality : would a senior engineer approve this in a code review?
  • Speed : time to a working solution
  • Vibe : how natural did the flow feel? Did I feel like I was driving or fighting?
  • Production readiness : edge cases handled, error states covered, tests included?

The 10 Tools

Cursor, Windsurf, Claude (claude.ai), GitHub Copilot (agent mode), Bolt.new, v0 by Vercel, Replit Agent, Devin, Aider and Codeium.

Tool 1: Cursor

Code quality: 9/10 | Speed: Fast | Vibe: 9/10 | Prod ready: 8/10

Cursor is the benchmark that everything else gets compared against and the comparison is usually unfair to everything else.

The React dashboard task: I described what I wanted in the chat sidebar. Cursor read the existing file structure, understood the component patterns I was using and produced a dashboard that matched my codebase conventions without me specifying them. The chart component needed one round of iteration, the initial output used a library I didn’t have installed, but the correction was a single message.

The debug task is where Cursor genuinely impressed me. I pasted the error logs and described the symptom. Cursor identified the async context manager issue in the database connection handling without me pointing it out. It explained why the bug caused intermittent failures specifically under load, not in isolation. That explanation was accurate and it’s the kind of contextual reasoning that makes the debugging session feel like pairing with a capable engineer rather than using a tool.

The refactoring task: clean extraction of concerns, appropriate abstractions, preserved behaviour. The one gap was that tests weren’t generated automatically, I had to ask for them separately.

The vibe is consistently good. The tab completion alone changes how fast you work. The chat integration with the file context feels natural. If you’re not using Cursor and you’re writing code daily, you’re leaving velocity on the table.

Tool 2: Windsurf

Code quality: 8/10 | Speed: Fast | Vibe: 8/10 | Prod ready: 7/10

Windsurf’s Cascade mode is the closest competitor to Cursor and in some tasks it’s genuinely better. The multi-file coordination, when a change in one file should propagate to related files, is handled more proactively than Cursor in my testing.

For the React dashboard, Windsurf’s output was slightly more boilerplate-heavy than Cursor’s. The structure was correct but the styling choices felt generic in a way that would need cleanup before shipping. Not wrong, just not as convention-aware.

The debugging task showed the gap: Windsurf identified the right area of the code but its explanation of why the bug manifested under load was less precise than Cursor’s. The fix was correct. The understanding behind it felt shallower.

The vibe is good, particularly in Cascade mode. Where Cursor feels like a co-pilot who reads your intent, Windsurf feels like a capable pair programmer who needs slightly more explicit direction. The distinction matters on complex tasks and disappears on simple ones.

Tool 3: Claude (claude.ai)

Code quality: 9/10 | Speed: Medium | Vibe: 7/10 | Prod ready: 9/10

Claude’s code quality is consistently the highest of any tool I tested. The React dashboard output was clean, well-commented, accessible and included error boundary handling I hadn’t asked for. The refactoring was architecturally thoughtful in a way that reflected genuine understanding of why the original code was problematic.

The debugging task: Claude caught the async issue, explained it with more depth than any other tool and provided a test case that would reproduce the bug reliably, something I hadn’t asked for.

The vibe score reflects the interface constraint. Claude in the browser is a chat interface, not an IDE. The code quality is excellent but the workflow of copy-paste between the chat and my editor breaks the flow that Cursor and Windsurf maintain natively. When Claude gets API access to your IDE (this is coming), the vibe score changes.

For code review and architectural reasoning, Claude is the best tool here. For the integrated vibe coding flow, the interface is the limitation.

Tool 4: GitHub Copilot (Agent Mode)

Code quality: 7/10 | Speed: Very fast | Vibe: 8/10 | Prod ready: 6/10

Copilot’s agent mode is fast. Tab completion that anticipates your next line before you’ve finished the current one is genuinely addictive. For boilerplate-heavy tasks, setting up a new component structure, writing standard CRUD operations, nothing is faster.

The gaps appear on complex tasks. The React dashboard output was functional but shallow, no error handling, no loading states, no edge case coverage. The structure was correct; the completeness wasn’t there.

The debugging task was the weakest performance of any tool I’d consider recommending. Copilot identified the general area of the problem but missed the specific async context issue, suggesting a fix that would have helped in some cases but not addressed the root cause.

If you’re primarily writing code and want faster typing, Copilot is excellent. If you’re solving complex problems and want to understand them, it underperforms the tools with more reasoning depth.

Tool 5: Bolt.new

Code quality: 7/10 | Speed: Very fast | Vibe: 8/10 | Prod ready: 5/10

Bolt.new exists in a different category from the IDE-integrated tools. It’s for generating full applications from descriptions, not for coding workflows within existing projects.

For the React dashboard, built from scratch, not integrated into an existing codebase, Bolt.new produced something visually impressive and functionally limited within about four minutes. The demo looks great. The code quality underneath is the kind that works until you need to change something.

For the debugging and refactoring tasks: Bolt.new isn’t designed for this use case and it showed. These tasks require context about an existing codebase that Bolt.new’s interface doesn’t support well.

The vibe for greenfield work is genuinely good, describing a product and watching it appear is still impressive even if you’ve seen it a hundred times. The production readiness of the output is not there for anything beyond prototyping.

Tools 6–10: The Quick Summary

v0 by Vercel : Excellent for React UI components specifically, poor outside that domain. Design sensibility is the best of any tool here. If you’re building Next.js frontends, v0 is a genuine productivity multiplier for component generation.

Replit Agent : Best if you need cloud deployment built into the workflow. The code quality is adequate, the integrated deployment is the differentiator.

Devin : The most autonomous of any tool. Genuinely impressive on multi-step tasks. The latency is real, it thinks before acting and the thinking takes time. For complex, long-horizon tasks where you want to describe an outcome and walk away, Devin is the tool. For interactive vibe coding where you want fast iteration, it’s too slow.

Aider : The power user’s choice. Terminal-native, works with any model, extremely configurable. The vibe is terminal-flavoured, excellent for developers who live in the command line, alienating for everyone else. Code quality is high when you configure it well.

Codeium : Strong autocomplete, adequate chat. The free tier is genuinely competitive with Copilot for basic completion. Less impressive on complex reasoning tasks.

The Honest Answer to “Does It Work for Production?”

Yes, with the right tools and the right mindset.

The vibe coding workflow produces production-quality code on well-defined tasks with tools like Cursor and Claude. The catch is that “well-defined” is doing work in that sentence. Vibe coding amplifies your ability to execute on a problem you understand, it doesn’t replace the need to understand the problem.

The failure mode I saw consistently: developers who described what they wanted without understanding the constraints or edge cases, accepted the first output without critical review and discovered the gaps when the code ran in a real environment.

The success mode: developers who used vibe coding to accelerate the implementation of problems they’d already thought through, treated AI output as a first draft rather than a final answer and maintained the ability to read and understand the code that was generated.

The tools that produce the best production code are the ones with the deepest reasoning capability, Cursor, Claude, Aider, not the ones with the fastest output. Speed is a feature. Understanding the problem is still your job.

For the full ranked comparison with screenshots, prompting strategies and code sample comparisons across all ten tools, Dextra Labs tested all 10 vibe coding tools head-to-head with the detail that a single Dev.to article can’t cover.

The full explainer on what vibe coding is, including the workflow patterns that work in production versus the ones that produce demo-quality code, covers the methodology in more depth.

Published by Dextra Labs | AI Consulting & Enterprise Development

The SARIF Viewer Is Now Available in CLion 2026.1.2

Starting with v2026.1.2, CLion includes the SARIFThe Static Analysis Results Interchange Format. Viewer, available out of the box. This is especially useful for embedded and automotive teams, for whom external static analysis is often part of the compliance toolchain. If you use tools that produce SARIF reports – such as Parasoft C/C++test, Clang Static Analyzer, or in-house checkers – you can now review their findings directly in the IDE, navigate to the relevant source code, and keep the triage process within your development workflow.

What is SARIF?

SARIF provides a common format for analyzing issues, rules, severities, code locations, and execution flows. That matters when a project relies on multiple tools at once, such as C/C++ analysis, compiler checks, and internal quality gates.

DOWNLOAD CLION

Why the SARIF Viewer matters for automotive teams

MISRA, AUTOSAR, CERT C/C++, and ISO 26262 reports no longer need to live only in a CI artifact, a web dashboard, or a vendor-specific tool. You can open them in CLion next to the code you’re working on.

How it works

  1. Select Use Code | Import SARIF Results… and then select your .sarif or .sarif.json file. You can also drag the report into the IDE’s Project tool window. CLion will validate the report, add a SARIF Results tab to the Problems tool window, and group findings by tool and rule.
  2. Double-click a result to open the corresponding location in the editor.

Imported reports are stored per project, so the triage state is restored when you reopen the project. You can also clear, re-import, filter, and group results from the SARIF Results toolbar.

Disabling the plugin

If your project doesn’t use SARIF reports, you can disable the plugin from Settings | Plugins | Installed | SARIF Viewer.

Try it in CLion 2026.1.2

The SARIF Viewer is available in CLion 2026.1.2 out of the box. Try it with your own reports or any other SARIF 2.1.0-compatible output, and let us know what would make the workflow better for you.

DOWNLOAD CLION

A Practical Guide to Profiling in Go

As is often the case with Go, the standard library comes with a great tool for profiling your programs – pprof. It samples your call stack, allowing you to later generate reports that help you analyze and visualize your software’s performance without installing any plugins. Everything you need is in the Go development kit.

The problem? It’s a bit of a hassle. In our discussions with Go developers, we’ve heard that some actually avoid it if they can. There could be a few reasons for this. For many developers, typical Go services perform well enough without optimizations, so when they do need to use profiling, it becomes a complex “rescue mission” tool they aren’t really experienced with. For some, the issue isn’t profiling in itself, but rather what to do with the results. Since pprof just shows developers a lot of low-level profiling data, it’s on them to make sense of it and find the root of the issue. On the other end of the spectrum, there are those who practice continuous profiling and use dedicated tools for it.

This article serves as a practical guide for those developers who would rather avoid dealing with Go’s confusing profiling tools. Profiling is incredibly useful – it helps you identify CPU bottlenecks, memory issues, and concurrency problems, all of which affect both your and your users’ experience with your product. So to help you make the best use of it, we will explain some of the main profiling types in Go (CPU, heap, allocs, mutex, block, and goroutine), as well as how to run and interpret them. And because you’re on the JetBrains blog, we’ll also show you how GoLand makes profiling as easy as pressing a single button. But first…

How does profiling work in Go?

Go profilers track program performance by sampling the call stack and additional data at either regular time intervals or upon specific runtime events, depending on the profile. They generate profile files that can then be analyzed using tools like the pprof CLI or its web interface, so you can see where your program spends time and memory. This helps you find functions that use unnecessary resources and slow down the program, without having to guess. For example, Go’s diagnostic documentation recommends profiling to identify expensive or frequently called code paths.

Types of profiles in Go

As mentioned in the intro, Go comes pre-equipped with a profiling tool called pprof, so you don’t need any external libraries. There are different things you can analyze with it, depending on your needs. The most popular profiles that we’ll be discussing in this article are:

  • CPU: Samples the call stack and tracks where CPU time was spent.
  • Memory (allocs / heap): Tracks allocations (total / currently in use) to show you where memory is being used.
  • Block: Tracks blocking events, showing you where goroutines were blocked.
  • Mutex: Captures which goroutines blocked other goroutines, revealing lock contention.
  • Goroutine: Takes snapshots of stack traces of goroutines to show you how many there are at the moment, and what they’re doing.

It’s perhaps worth mentioning here that Go also has the runtime/trace package – an execution tracer that records specific runtime events, capturing the timeline rather than snapshots. runtime/trace will not be covered in this article.

CPU

CPU profiling is often the first step when diagnosing performance issues in Go programs. It records where your program spends CPU time by periodically sampling the stack of the goroutines that are being executed.

It’s good for things like finding hot paths in CPU-bound code (e.g. expensive parsing, serialization, hashing, or tight loops), understanding why a benchmark is slower than expected under realistic load, investigating the root cause of a Grafana alert, or generating input for profile-guided optimization.

What this profile does not tell you is where your program spends time waiting on locks or the network. Since CPU profiling samples active execution, blocking and contention need other profiles, like the block and mutex ones described below. This means the actual running time of a goroutine will not match its execution time on the CPU.

Memory profiles – heap and allocs

The memory profiles – heap and allocs – are perhaps the most confusing, even for seasoned Go developers. To clarify: heap and allocs are both types of memory profiling that give you insights into memory consumption, allocation patterns, and garbage collection (GC). Under the hood, both store the same data. The only difference is which sample type they present as the default.

The sampling types available in both profiles are:

  • inuse_space: The amount of memory (in bytes) that’s currently allocated and has not yet been garbage collected.
  • inuse_objects: The total number of individual objects currently on the heap.
  • alloc_space: The cumulative amount of memory (in bytes) allocated since the program started (including memory that has already been freed).
  • alloc_objects: The cumulative count of objects allocated since the program started (including those that have already been collected).

The heap profile shows inuse_space as the default view, and the allocs profile shows alloc_space. You can, however, switch between all four sampling types freely.

An important point about memory profiles is that they are sampled, not exact. Go’s A Guide to the Go Garbage Collector explains that, by default, these profiles only sample a subset of heap objects that’s good enough to find hotspots.

Another thing to remember is that memory profiles don’t actually cover all memory, as Go can allocate some values to the stack and outside the heap that’s managed by GC, depending on the outcome of escape analysis.

Block profile

The block profile shows you where goroutines are blocked waiting for synchronization primitives such as sync.Mutex, sync.RWMutex, sync.WaitGroup, sync.Cond, and channel send/receive/select. A block profile tracks blocking events, measures how long they last, and then aggregates them by stack trace once they are completed. It tells you where your program spent time waiting instead of doing useful work and helps you optimize inefficient synchronization patterns.

The block profile tracks two sample types:

  • Contentions: This shows the number of times a block event occurred (i.e. multiple goroutines attempted to access a shared resource simultaneously and only one could proceed).
  • Delay (latency): This shows the total time spent being blocked (i.e. the actual amount of time a goroutine spent in a blocked state before it could resume execution).

This is an important distinction because you can have low contention with high delay (and vice versa); for example, when only one goroutine waits for a lock, but it takes 10 seconds because the holder is performing a slow network call.

Block profiling is disabled by default in the Go runtime, as it introduces overhead. In production, you should enable block profiling only for very short periods and at a very long sampling interval to investigate known issues. 

Mutex profile

In contrast to the block profile, the mutex profile captures goroutines that block other goroutines and focuses specifically on sync.Mutex and sync.RwMutex contention. You could say that, while the block profile tells you what is waiting, the mutex profile tells you what is causing the wait. Another significant difference is that mutex profiling uses event-based sampling rather than time-based sampling.

On the other hand, the mutex profile behaves similarly to the block profile in that it only records completed events and is also disabled by default. It also tracks two sample types – contentions and delay.

You should reach for a mutex profile when you think your application throughput or latency is being limited by lock contention. The Go diagnostic docs explicitly recommend using it when the CPU is not fully utilized because of mutex contention.

Goroutine profile

As the name would suggest, goroutine profiling helps you inspect how many goroutines exist in your program and what they’re doing at the moment by taking a snapshot of their stack traces. The goroutine profile helps you debug concurrency issues by identifying goroutine leaks or deadlocks as they are happening

In the context of block and mutex profiles, it’s critical to remember that the goroutine profile deals with current goroutines. Every entry in the profile shows the current function call stack, whether the goroutine is running, waiting, or blocked, and where it’s stuck (channel, mutex, I/O, etc.). The profile is exposed in net/http/pprof by default, which makes it the go-to choice for troubleshooting your program when it’s hanging or experiencing a pile-up.

That said, when you’re investigating problems with your program, it’s best to look at all three profiles – goroutine, block, and mutex – to get the full picture. For example, if you see a pile-up in the goroutine profile that shows multiple goroutines parked in sync.Mutex.Lock, the block profile will tell you which callers spent time blocked there, and the mutex profile will tell you which section made the wait expensive.

How to collect Go profiles

Now that we know what the main profiles in Go are, let’s see how you can collect and interpret them to actually improve your software.

There are different ways to collect the profiles – with runtime/pprof, net/http/pprof, and… GoLand. They all produce pprof-compatible profiles that you can then visualize with either go tool pprof or GoLand.

While the “traditional” ways are somewhat tedious, if you know how to run one profile, you know how to run the others – for the most part. We’ll go through the general steps and point out differences and exceptions where necessary.

And for those of you who have GoLand version 2026.1.2 or higher, we’ll show you how to run and inspect the profiles without having to remember any commands or a single line of code. 

runtime/pprof

For explicit, code-controlled profiling, runtime/pprof is the way to go. For most applications, this is the most direct API:

import (
	"os"
	"runtime"
	"runtime/pprof"
)

func captureCPU() error {
	f, err := os.Create("cpu.pb.gz")
	if err != nil {
		return err
	}
	defer f.Close()

	if err := pprof.StartCPUProfile(f); err != nil {
		return err
	}
	defer pprof.StopCPUProfile()

	runLoad()
	return nil
}

func captureHeap() error {
	runtime.GC() // run GC first to capture only the most current objects
	f, err := os.Create("heap.pb.gz")
	if err != nil {
		return err
	}
	defer f.Close()
	return pprof.Lookup("heap").WriteTo(f, 0)
}

func captureAllocs() error {
	f, err := os.Create("allocs.pb.gz")
	if err != nil {
		return err
	}
	defer f.Close()
	return pprof.Lookup("allocs").WriteTo(f, 0)
}

func dumpGoroutines() error {
	return pprof.Lookup("goroutine").WriteTo(os.Stdout, 2)
}
  • As you can see, the CPU profile uses StartCPUProfile / StopCPUProfile. It has its own start/stop API because it streams during a time window – you turn it on, run the workload, and then stop it when it’s finished.
  • The heap profile captures data since the last GC, so you should call runtime.GC() before writing a profile. Forcing the GC will give you the most up-to-date stats, but there are some scenarios where you might want to avoid that.
  • For goroutine profile, text output is usually better than pprof graphs when you’re debugging a leak or a deadlock, hence pprof.Lookup("goroutine").WriteTo(os.Stdout, 2).

net/http/pprof

The standard choice for long-running services is net/http/pprof. You start by importing the package:

import (
	"log"
	"net/http"
	_ "net/http/pprof"
)

func main() {
	go func() {
		log.Println(http.ListenAndServe("localhost:6060", nil))
	}()

	runServer()
}

Here are the typical live commands:

# CPU: 30-second profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Live heap, forcing a GC first
go tool pprof http://localhost:6060/debug/pprof/heap?gc=1

# Total allocations since process start
go tool pprof http://localhost:6060/debug/pprof/allocs

# Block profile (only useful if SetBlockProfileRate was configured)
go tool pprof http://localhost:6060/debug/pprof/block

# Mutex profile (only useful if SetMutexProfileFraction was configured)
go tool pprof http://localhost:6060/debug/pprof/mutex

# Human-readable goroutine dump
curl http://localhost:6060/debug/pprof/goroutine?debug=2
  • For CPU, seconds=N means “record the CPU profile for N seconds”. The HTTP handler for CPU defaults to 30 seconds if you omit the parameter.
  • For heap, heap?gc=1 will run the GC, while heap and heap?gc=0 will not.
  • debug=1 switches the profile to text (with the exception of the CPU profile).

If you want a quick and easy way to take a look at the profiles, head straight to http://localhost:6060/debug/pprof – you’ll find an HTML index listing all the available profiles there.

Capturing profiles with GoLand

If you don’t work with profiles regularly, collecting them the traditional way usually requires you to look up the documentation or tutorials to first remember what, where, and when. That’s why, in GoLand 2026.1.2, we implemented a new profiler tool that makes it easier for you to both collect and inspect profiles – all from the comfort of your IDE.

The tool currently allows you to capture CPU, heap, allocs, goroutine, block, and mutex profiles, as well as view their imports.

Opening the profiler

There are multiple ways to start the profiler tool.

  • Go Performance Optimization tool window: In the tool window bar, you will see a new icon for the Go Performance Optimization tool, which is where profiling actions can be performed. From there, you can choose which configuration to run, view profiles you already captured, or import profiles captured by someone else.
  • Run widget: Now, in addition to running and debugging, this widget also includes an option to profile your program, available under the More Actions icon. Clicking on the Profile with… option will set up the process and open the Go Performance Optimization tool window.
  • Gutter icon: Wherever you see the run icon in the gutter, you can launch the profiler tool from the dropdown that appears when you click on it.

Capturing profiles

Capturing profiles is pretty straightforward. To launch your program in profiling mode, click the Run with Profiler button – GoLand will set up the entire process for you. To capture the profile you’re interested in, simply click on the corresponding button.

An important caveat is that when profiling is initiated, GoLand modifies the executable during compilation to expose the required profiling API. Your code and your program will not be affected, but for that reason, the profiler can’t be attached to a program that’s currently running.

Capturing a CPU profile

Since a CPU profile is not collected continuously or by default, it works a little differently from the other profiles. In order to run it, you have to click Start CPU recording, let the profiler run for the desired amount of time, and then stop the recording to inspect the results.

Capturing a heap profile

GoLand allows you to capture a heap profile with or without forcing garbage collection first. Normally, the advice is to run the GC before collecting a heap profile since it reports memory stats as of the most recently completed GC cycle and skips more recent allocations to avoid skewing the profile toward short-lived garbage. Therefore, if you want to see the heap objects that are currently live, you should force GC first.

There are, however, scenarios where you don’t want that, and then it makes sense for you to collect the heap profile without GC:

  • You want to see the process in its natural state. Forcing GC changes the heap, GC pacing, and short-term memory behavior. If you want to see what the service looks like under normal load, avoid it.
  • You’re looking into allocation pressure or memory spikes. When you want to investigate how much work the GC has to actually do, forcing a cleanup will hide the “mess” that you’re trying to investigate.
  • You’re comparing the states before and after forced GC. If memory disappears after garbage collection, it was probably garbage that was waiting for collection or GC pacing. If it remains, you have retained objects.

Note that if you capture the heap profile with GC first, GoLand will actually force garbage collection, so capturing a heap profile without GC right after will likely not provide meaningful insights.

Importing and exporting profiles

The Profiler tool also allows you to open and view profiles captured by others (not necessarily using GoLand), as long as they’re in a pprof-compatible format. Simply drag and drop the file, or click the Import button in the Go Performance Optimization tool window and select the file from there.

Profiles captured with GoLand are pprof-compatible and stored in a designated directory. If you want to share a profile you captured in GoLand, go to Recent profiles and right-click on the one you want to share to reveal its location.

How to inspect profiles

Once you have a Go profile, inspection is mostly the same, no matter how it was collected, since go tool pprof can read either a saved file or a live HTTP URL.

In the terminal

If you have a saved profile file collected with runtime/pprof, you can access it in the terminal:

go tool pprof ./your-binary cpu.pb.gz

This will start pprof’s interactive shell in the terminal. The main text reports are top, list, tree, peek, and traces.

If you want a one-shot terminal report instead of an interactive shell, add a format flag:

go tool pprof -text ./your-binary cpu.pb.gz
go tool pprof -tree ./your-binary mutex.pb.gz
go tool pprof -peek='mypkg.(*Cache).Get' ./your-binary cpu.pb.gz
go tool pprof -list='mypkg.(*Cache).Get' ./your-binary cpu.pb.gz

This will print the report and exit the profile.

From the web interface

This is the easiest way if you want to see graphs and flame graphs, and move between top, peek, and source views.

# saved local profile
go tool pprof -http=localhost:8081 ./server cpu.pb.gz

# live profile from a running service, with the local binary for symbols/source
go tool pprof -http=localhost:8081 ./server 
  'http://localhost:6060/debug/pprof/profile?seconds=30'

With -http=host:port, pprof starts a local web server and opens a browser. The web UI provides multiple views of the same profile:

  • Graph: Visualizes the call graph.
  • Flame Graph: Provides an interactive flame graph (the larger the node and the thicker the edge, the higher the resource consumption is).
  • Top: Lists the functions consuming the most resources in a table format.
  • Source: Displays source code annotated with resource consumption.
  • Peek: Provides a statistical peek at function samples.

Inspecting profiles with GoLand

Navigating between your code and different endpoints, as is the case with go tool pprof, can be quite distracting. The profiling tool in GoLand lets you see and manage everything related to profiling in one place – your IDE.

Once you collect or import your profiles, you will have access to views that you probably recognize from the web interface – (call) graph, flame graph, and top – as well as a tree view. You can access these views for all the profiles (CPU, heap, allocs, goroutine, block, and mutex) and select different sample types:

  • The CPU profile can show you either CPU time or samples.
  • The heap profile, by default, shows you in-use space, but it can also show in-use objects, allocated space, or allocated objects
  • The allocs profile, by default, shows allocated space, but it can also show you everything that the heap profile does. That’s because these two are both representations of the same memory profile, which we discussed earlier.
  • Mutex and block both can show either contentions or delay.
  • The goroutine profile only has one sample type – number of goroutines – so there is no selector for that profile.

You can easily navigate directly to the relevant line of code from any of the views, simply by clicking on the relevant function. The tool will also highlight any hotspots in your code with a fire icon in the editor’s gutter.

Now that you know what profiles you can visualize, let’s discuss in more detail what the different views show and how to use them.

Top

The Top view shows you an ordered table of the top functions in the profile. The values in this view represent different units, depending on the profile (e.g. CPU time, memory bytes, or goroutine counts).

  • Flat and Flat%: The amount of the resource consumed directly by that function, excluding callees. Flat values tell you if the function itself is expensive.
  • Sum%: Running sum of Flat%.
  • Cumulative and Cumulative%: The total amount of the resource consumed by this function and everything it calls. This tells you if the function creates a lot of work overall.

By default, the functions in this view are ordered from the highest to lowest Flat. However, you can reorder the view however you like – clicking on any other header will make it the default for ordering, and clicking on the same header again will reverse the order.

Clicking on a function’s row will also take you to the relevant line of code in the editor.

Graph

In the web interface, a call graph is the default way of visualizing the data as a network. We’ve taken efforts to make the Graph view more visually inviting and interactive.

The nodes of the graph are functions – the redder the box, the more resource-intensive the function. The edges are function calls labeled with how much data (e.g. CPU time or memory) flows in that call – the bolder and redder the arrow, the more resources the call consumes. The dashed lines represent paths through nodes that have been removed from view because of the graph settings (such as node count). You can move both the edges and the nodes to improve visibility if the graph looks too dense.

The Graph view in GoLand allows you to adjust the view to your needs by modifying the node and edge fractions:

  • Node count: Limits the number of nodes shown on the graph to the top N nodes. For example, if you set your node count to N=80, the graph will only show 80 nodes (regardless of the remaining settings). Visible nodes are determined by a special entropy-based algorithm from the pprof tool (for example, the algorithm favors nodes with high cumulative or flat values and diverse call patterns, while deprioritizing simple passthrough nodes).
  • Node fraction: Controls which nodes will be shown, based on how much of the total sample value a node accounts for. For example, if you set the node fraction to 0.01, the graph will only show functions responsible for 1% or more of the total samples. The smaller the value, the more detail you will see, but this may also lead to more clutter that will make it harder for you to notice hotspots.
  • Edge fraction: This setting works similarly to the node fraction, but controls which edges (the call paths between functions) are shown. For example, if you set it to 0.005, the graph will only show call edges that contribute 0.5% of the total samples.
  • Call tree: This toggle changes how the data is presented by showing the actual paths, which means the same function can appear multiple times in different contexts. By default, instead of a call tree, the graph shows a call graph, where functions are merged so there’s only one node per function.

If you look at the default graph and find it overwhelming, try increasing the fractions to see fewer nodes or edges, or reduce the node count. If you feel like important paths are missing, decrease the node or edge fractions.

Flame graph

The Flame graph is the default view when you open a profile, as our research has established that this is the view developers access the most often.

A flame graph is a visualization of the call stack where cost is presented as width, i.e. the longer the bar, the more resources the function and its children are using. The height only signifies the depth of the call chain, so a tall but skinny tower will not be your culprit when it comes to resource consumption.

If you hover over a function, you will see the cumulative percentage (% of all), the share relative to the parent (% of parent), and cumulative value – the same data you can also see in Top and Tree views. Clicking on a function’s bar will take you to the relevant line of code, and if that function is hot, there will be a flame icon in the gutter.

Some other interesting functions in this view are:

  • Icicle graph: The flame graph by default is presented in an icicle view, i.e. main is at the top. You can uncheck that option to turn the view upside down.
  • Capture image: You can take a snapshot of the graph and save it or copy it to the clipboard.
  • Search: You can search for the function you’re interested in.

You can also try out the New Flame Graph view option for a more modern look and feel.

Tree

You can also see the Tree view. This view presents the same data as the Top view, but organizes all functions under their parent function, regardless of how resource-intensive they are.

Line profiler

The line profiler is an alternative to the -list command in pprof

Once you run your program with the profiler tool, GoLand will add runtime hints near the corresponding lines of code right in the editor. Lines that took a significant amount of time to execute will have grey labels, while the most resource-intensive ones will be marked with red labels with a fire icon.

The connection works the other way round as well. You can navigate directly to a specific line of code in the editor from any of the profiling views, simply by clicking on the relevant function.

Why try GoLand’s profiler?

The profiler tool in GoLand makes profiling your software as easy as clicking a button. You no longer need to remember commands or additional steps. The entire process stays in your IDE, so you don’t have to jump between the editor and the browser, and you can easily check the problematic lines of code. We hope that both the new tool and this guide will take the guesswork out of profiling and help more developers make this code optimization technique a part of your daily work.

Happy coding!
The GoLand Team