Julia 1.13 Highlights

10 September 2026 | The Julia contributors

Julia version 1.13 has been released. We want to thank all the contributors to this release and all the testers who helped find regressions and issues in the pre-releases. Without you, this release would not have been possible.

The full list of changes can be found in the NEWS file, but here we'll give a more in-depth overview of some of the release highlights.

  1. Latency (TTFX) improvements
  2. REPL improvements
    1. Syntax highlighting
    2. New fzf-style history search
    3. Bracketed paste on Windows
  3. @__FUNCTION__
  4. Hashing changes
  5. Faster GC by skipping image objects during marking
  6. Scheduler and interrupt fixes
  7. Introspection with type annotations
  8. Tracing top-level evaluation with --trace-eval
  9. JuliaC/trim
  10. Pkg
    1. Change in default compression algorithm from gzip to zstd
    2. Performance improvements
    3. Registries for packages tracked in the manifest
    4. Recursively collect sources
    5. pkg> add now tries to add the same version as already-loaded packages
    6. Pkg.test no longer defaults to enabling strict bounds checking
  11. Juliaup GUI
  12. Acknowledgement

Latency (TTFX) improvements

Ian Butterworth, many others

Julia 1.13 takes roughly 30% less time to precompile packages than 1.12, and roughly 10-20% less time than 1.10 (LTS) depending on the machine.

Time To First X (TTFX), the time from starting Julia to getting a first result, is made up of three main costs: precompiling packages, loading them, and running the code. With the help of the community-submitted workflows at Julia-TTFX-Snippets, we have started measuring these costs more systematically on real-world examples and optimizing Julia against them.

The chart below shows the geometric mean across all 39 currently submitted workflows, on two machines. Precompilation is the fastest of 2 runs; load and execution times are the fastest of 3 runs.

This monitoring is now also part of Julia's own development process: new TTFX CI jobs run on relevant pull requests and on every commit to master, and the results are tracked at perf.julialang.org/ttfx. That tracking went live on September 7, 2026; measurements before then were ad hoc.

Julia 1.13 startup is also ~20% faster than 1.12.

% hyperfine --warmup 3 --runs 20 -N \
  --command-name "julia 1.12" "julia +1.12 --startup-file=no -e ''" \
  --command-name "julia 1.13" "julia +1.13 --startup-file=no -e ''"
Benchmark 1: julia 1.12
  Time (mean ± σ):      69.1 ms ±   1.0 ms    [User: 50.1 ms, System: 18.1 ms]
  Range (min … max):    68.0 ms …  72.6 ms    20 runs

Benchmark 2: julia 1.13
  Time (mean ± σ):      56.7 ms ±   0.5 ms    [User: 49.1 ms, System: 18.9 ms]
  Range (min … max):    56.0 ms …  58.1 ms    20 runs

Summary
  julia 1.13 ran
    1.22 ± 0.02 times faster than julia 1.12

REPL improvements

Syntax highlighting

Timothy, Kristoffer Carlsson

The Julia REPL now has syntax highlighting (without having to load an external package like OhMyREPL.jl):

REPL syntax highlighting

By default, the color scheme is quite conservative, but it is easy to customize (see the documentation for the REPL). As an example, here is the same code but using the Monokai color scheme:

REPL syntax highlighting with the Monokai color scheme

Timothy

The history search (entered by default via Ctrl-R) has been redesigned and now works similarly to the command-line fuzzy finder fzf:

REPL history search

REPL history search for LinearAlgebra

Among other things, the new history search has support for:

Enter the history search and type ? to see the full help.

Bracketed paste on Windows

Bracketed paste allows an application running in a terminal to know when text is being pasted (as opposed to just being typed). This can allow for more efficient and correct processing of the text being pasted. This functionality has been enabled on Linux and macOS for a long time but is now also finally available on Windows. As a concrete example, the videos below show the behavior of pasting a ~500-line function into the Julia REPL before and after enabling bracketed paste on Windows.

Before:

After:

@__FUNCTION__

Miles Cranmer, Jeff Bezanson

Like the existing @__MODULE__ and @__FILE__ macros, the new @__FUNCTION__ macro references the innermost containing function even if that function is anonymous. This should work in all kinds of functions, and is public API, unlike the internal variable #self#.

julia> fact = n -> n <= 1 ? 1 : n * @__FUNCTION__()(n - 1);

julia> fact(5)
120

Hashing changes

Andy Dienes, Jameson Nash

The hash function has been replaced. The byte-hashing algorithm is now RapidhashNano. This hash is used by default for AbstractString and many numeric types like BigInt, Rational, and large Real or Integer values. It is also much easier now for custom types to opt in to the generic implementations without having to first convert to a supported type (like String). This change offers several advantages compared to the pre-existing implementation based on MurmurHash3. It has significantly better performance, is a streaming hash so it no longer requires the length of the input up front, and has moved from C to pure Julia for better readability and maintainability.

To demonstrate the performance improvement on long strings:

using BenchmarkTools, Downloads

io = IOBuffer()
Downloads.download("https://www.gutenberg.org/cache/epub/1080/pg1080.txt", io)
s = String(take!(io));

# 1.12
@btime hash($s)
  8.555 μs (0 allocations: 0 bytes)
0x5fbd2717019846ea

# 1.13
@btime hash($s)
  1.742 μs (0 allocations: 0 bytes)
0x718308e795047519

And a demonstration of opting in to a faster fallback:

struct MyString <: AbstractString
    s::String
end
m = MyString(s);

# 1.12
Base.iterate(m::MyString) = iterate(m.s)
Base.iterate(m::MyString, i::Integer) = iterate(m.s, i)
@btime hash($m)
  204.583 μs (21 allocations: 107.02 KiB)
0x5fbd2717019846ea

# 1.13
Base.codeunit(m::MyString) = codeunit(m.s)
Base.codeunits(m::MyString) = codeunits(m.s)
@btime hash($m)
  1.750 μs (0 allocations: 0 bytes)
0x718308e795047519

The hash for small fixed-width data has also changed. The final mixing step is now a single-round XMX construction with some carefully tuned constants, and the mixing step now properly avalanches when composing hash calls; previously the mixing step always simplified to a linear function at every composition depth. This change to the mixing step does introduce a data dependency (and thus potentially lower performance) when sequentially hashing elements together in a tight loop, e.g. foldr(hash, collection), but the algorithm for hashing AbstractArray has been partially unrolled at small to medium sizes, maintaining several hash accumulators in parallel, and will be much faster at most lengths.

Some important reminders: hash remains noncryptographic. Also, the default seed has changed. Custom hash methods should always accept the seed as an argument like hash(x::MyType, h::UInt) and never provide a default value like hash(x::MyType, h::UInt=0), since the correct seed is determined by the caller.

Faster GC by skipping image objects during marking

Cody Tapscott

Every Julia session starts with a large number of objects that were loaded from the system image, and every package that gets loaded brings its own package image with even more of them: method tables, type information, compiled code, constants and so on. These objects are never freed, and they are rarely mutated, yet until now a full garbage collection would walk through all of them to mark them as reachable, just like any other object on the heap. For a session with a handful of large packages loaded, this could easily be the dominant cost of a full collection.

In Julia 1.13, objects in the sysimage and in package images are loaded as permanently marked and the mark phase never enters them. The few mutations that do happen to image objects (for example, when a method is added to an existing function) are tracked separately so that any new objects they point to are still kept alive. The effect is that the cost of a full collection now scales with the size of the heap that your program actually created, not with the amount of code that has been loaded.

The easiest way to see the difference is to time a full collection in a fresh session:

# 1.12
julia> @time GC.gc()
  0.035493 seconds (99.90% gc time)

# 1.13
julia> @time GC.gc()
  0.000528 seconds (99.08% gc time)

The table below shows the time for a full collection (GC.gc(true)) on an Apple M4 Pro, first in a bare session and then after loading some packages of increasing size. Incremental (young generation) collections are not affected by this change and are equally fast on both versions.

1.121.13
Bare session35 ms2 ms
using Revise50 ms11 ms
using Cthulhu59 ms18 ms
using PythonCall90 ms30 ms
using GLMakie187 ms68 ms

Since full collections are triggered more often for programs with a large live heap, this also shows up as reduced overall GC time in real workloads. The following example inserts random vectors into a Dict that is kept alive across iterations, so that a large fraction of the allocated objects get promoted to the old generation:

function work(n)
    d = Dict{Int,Vector{Float64}}()
    for i in 1:n
        d[i % 50_000] = rand(64)
    end
    return length(d)
end

# 1.12
julia> @time work(5_000_000)
  1.699095 seconds (10.00 M allocations: 2.688 GiB, 79.80% gc time)

# 1.13
julia> @time work(5_000_000)
  0.566276 seconds (10.00 M allocations: 2.688 GiB, 44.32% gc time)

For more details, see the pull request.

Scheduler and interrupt fixes

Kiran Pamnany, Jameson Nash, Ian Butterworth

Idle threads now park in a dedicated scheduler task instead of holding on to the last task they ran, so finished tasks can be garbage collected promptly (#57544). This lands alongside a set of related scheduler fixes, including ones that make interrupts reliable again (#62665):

Work on a proper task cancellation mechanism is in progress and is planned for Julia 1.14.

Introspection with type annotations

The code introspection macros (@which, @code_typed, @code_warntype, etc.) now accept call expressions where arguments are given as types instead of values, using the same ::T syntax as in method definitions and stacktraces. Values and types can be freely mixed, and keyword arguments are supported:

julia> @which push!(::Vector{Int}, 1)
push!(a::Vector{T}, item) where T
     @ Base array.jl:1339

julia> @which sort!(::Vector{Int}; by = ::Function)
kwcall(::NamedTuple, ::typeof(sort!), v::AbstractVector{T}) where T
     @ Base.Sort sort.jl:1734

This means a frame can be copied straight out of a stacktrace and pasted into @which to find the method that was called:

julia> @which Base.Order.lt(o::Base.Order.Lt{typeof(isless)}, a::Int64, b::Int64)
lt(o::Base.Order.Lt, a, b)
     @ Base.Order ordering.jl:121

Broadcasting expressions are also supported in @code_lowered, @code_typed and @code_warntype:

julia> @code_warntype (::Vector{Int}) .+ 1.0

Tracing top-level evaluation with --trace-eval

Ian Butterworth

The new --trace-eval command-line flag shows top-level evaluation progress, to help see how a test suite or script is advancing, e.g. to identify hangs. For instance:

% julia --trace-eval script.jl
eval: #= /Users/me/.julia/config/startup.jl:1 =#
eval: #= /Users/me/.julia/config/startup.jl:2 =#
eval: #= /Users/me/.julia/config/startup.jl:3 =#
eval: #= script.jl:1 =#
eval: #= script.jl:2 =#
Hello world

It is also enabled automatically when the "debug logging" option is turned on for a CI run, as shown here for GitHub Actions:

GitHub Actions re-run dialog with "Enable debug logging" checked

JuliaC/trim

Cody Tapscott, many others

The juliac.jl script in the Julia repo has been made into a proper package/application: JuliaC.jl.

More code can now be trimmed, such as finalizers, @cfunction and mapreduce.

Several bugs in the trimming process itself were also fixed, improving its reliability.

Pkg

Kristoffer Carlsson

Pkg has received quite a bit of attention for 1.13. Here we list some of the more notable changes and improvements.

Change in default compression algorithm from gzip to zstd

For downloads from a package server (registries, packages and artifacts), Pkg will now by default ask for a zstd-compressed archive instead of a gzipped one. For the type of files Pkg typically downloads, zstd compression tends to have both a better compression ratio and significantly better decompression performance. As an example, downloading the packages and artifacts for the packages Plots, Makie and ModelingToolkit results in the following numbers:

gzipzstd
Total downloads405405
Total download size307.99 MB239.31 MB
Total decompression time8.77 s5.50 s
Average decompression time21.98 ms13.77 ms

Performance improvements

Some micro-optimizations have been made to the resolver and the registry processing, leading to generally better performance of Pkg operations. Some of these improvements have already been backported to 1.12, so to get a proper performance comparison we compare against 1.12.1, which did not get any of these backports.

To assess the impact on resolver speed, we do the following benchmark: we add Plots to an empty environment, remove it, and then benchmark the time it takes to add Plots again. This ensures that all the files for Plots are already downloaded. In addition, auto-precompilation is turned off and the registry cache is cleared so that it has to be re-read from scratch. This means that the time spent adding Plots to this environment is mostly registry processing and resolving:

julia> ENV["JULIA_PKG_PRECOMPILE_AUTO"] = 0

# 1.12.1
julia> empty!(Pkg.Registry.REGISTRY_CACHE); @time Pkg.add("Plots"; io=devnull)
  1.257017 seconds (8.83 M allocations: 681.328 MiB, 16.31% gc time)

# 1.13.0
julia> empty!(Pkg.Registry.REGISTRY_CACHE); @time Pkg.add("Plots"; io=devnull)
  0.745170 seconds (4.43 M allocations: 304.580 MiB, 26.90% gc time)

In addition, Pkg will now clone repos with more efficient settings, avoiding downloading unnecessary data:

# 1.12.1
julia> @time Pkg.add(name="Plots"; rev="master")
     Cloning git-repo `https://github.com/JuliaPlots/Plots.jl.git`
...
 10.953074 seconds (4.51 M allocations: 330.819 MiB, 1.68% gc time)

# 1.13.0
julia> @time Pkg.add(name="Plots"; rev="master")
     Cloning git-repo `https://github.com/JuliaPlots/Plots.jl.git`
...
  2.980337 seconds (2.87 M allocations: 189.202 MiB, 3.87% gc time)

Registries for packages tracked in the manifest

Previously, to instantiate a manifest you needed to manually make sure that the registries required by that manifest were available. Now, the registry each package came from is recorded in the manifest and is automatically installed upon manifest instantiation (or other package operations).

Recursively collect sources

Pkg now recursively collects [sources] entries from packages fetched by URL, allowing private dependency chains to resolve without requiring all dependencies of a private package to be in a registry.

pkg> add now tries to add the same version as already-loaded packages

Julia has always allowed changing the active project during a session and supports stacked environments (most commonly via the default environment), which introduces a rough edge that can lead to repeated precompilation of packages. For instance, a version of a package is loaded from the default environment during startup.jl, and then the user adds a new package to the active project that pulls in a different version of that dependency. Pkg precompiles the dependency graph of the active project, so the new version gets precompiled even though the already-loaded version would often have satisfied the compat constraints just as well.

In 1.13, Pkg prefers the currently loaded version of any package that is already loaded when resolving pkg> add, if the environment's compatibility constraints allow it, so nothing needs to be precompiled again. As usual, pkg> status will flag that a newer version is available.

Pkg.test no longer defaults to enabling strict bounds checking

Previously, Pkg.test always launched the test process with --check-bounds=yes, which forces bounds checking even inside @inbounds blocks. Since precompile cache files are specific to the bounds-checking mode, this meant that the package being tested and all of its dependencies typically had to be recompiled before the tests could even start, and those cache files were then useless for normal development. Pkg.test now leaves the bounds-checking mode alone, so the test process inherits it from the parent Julia session and can reuse the precompile files generated during development. To get the old behavior, either start Julia with --check-bounds=yes before running Pkg.test, or pass the flag explicitly with Pkg.test(; julia_args=["--check-bounds=yes"]).

Juliaup GUI

Ian Butterworth

Juliaup, the Julia version manager, now has a graphical interface alongside its command line. It ships with Juliaup 1.22 and later on every platform Juliaup supports, so after a juliaup self update it can be opened with:

juliaup gui

The Installed tab shows each installed channel as a tile or a list row. From there a channel can be launched, launched with a custom project, arguments and environment variables, set as the default, or removed, and there are one-click actions to update everything and to garbage collect versions no channel uses any more.

The Juliaup GUI's Installed tab, showing installed Julia channels as tiles

The Available tab lists everything in the channel database, including release, lts, rc, nightly and pr{number} channels for testing pull requests, with an install button for each. It can also link an existing Julia binary to a custom channel name. The Configuration tab exposes Juliaup's settings, such as the version database update interval and automatic self-updates.

The Juliaup GUI's Available tab, listing channels that can be installed

Acknowledgement

The preparation of this release was partially funded by NASA under award 80NSSC22K1740. Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Aeronautics and Space Administration.