# Socket connection error after macOS 26.7 update

**URL:** <https://forum.crystal-lang.org/t/socket-connection-error-after-macos-26-7-update/9145>\
**Category:** Help & Support\
**Created:** [September 17, 2026, 11:45pm UTC](https://forum.crystal-lang.org/t/socket-connection-error-after-macos-26-7-update/9145 "2026-09-17T23:45:35Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![nogginly](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nogginly/32/1795_2.png) [@nogginly](https://forum.crystal-lang.org/u/nogginly)\
**Post date:** [September 17, 2026, 11:45pm UTC](https://forum.crystal-lang.org/t/socket-connection-error-after-macos-26-7-update/9145/1 "2026-09-17T23:45:35Z")

</div>

> UPDATE 2026-09-18 - Script `connect_refusal_check_v2.cr` replaces earlier version below, adding a test without O\_NONBLOCK, which turned out to be OK (whew!)

Hola folks,

I’m looking for folks who are running macOS and have, or are about to, update to macOS 26.7 (still Tahoe) to run a test. Here’s what happened:

- I have several Crystal apps that use HTTP to talk to local servers
- The local servers are accessible via `localhost` and `127.0.0.1`
- Until two days ago, all my apps worked fine talking to `localhost` as host.
- After the macOS point update, the same apps, not re-compiled, shipped binaries, all failed with a timeout error.
- Switching to `127.0.0.1` they worked; but no longer with `localhost`.
- But `curl` worked fine with both.

Intriguing, I thought! I pulled out my dweeb hat and empty pipe and leaned my unused violin nearby and sat down to some sleuthing. ;-)

- `Addrinfo.resolve` returns two entries for `localhost`: an v6 address `::1` and v4 address `127.0.0.1`
- `TCPSocket.new` gets these two and then calls `Socket#connect` which goes through them and is supposed to go past a failing `::1` and land on `127.0.0.1`, which it did in fact do _before my macOS update._
- However now it “thinks” it succeeded and returns a socket that then fails immediately when the `HTTP::Client` tries to write to it.

I’m the first person to line up and say “it must be me, can’t be Apple, seems too basic.” (I call this the “Oracle problem”, back in the day when fellow devs ran into SQL problems they blamed it on Oracle, but it was never Oracle.)

With help from Claude Opus I learned about how `curl` does something called “happy eyeballs” or RFC 8305. That alone was worth running this issue. But this still didn’t feel right. After some more research I was able to write a test program in Crystal that tries to extract all the different error codes and confirm / deny the issue.

Also, since this was affecting shipping software I needed to ship a fix, so I worked out a monkey-patched shard that I was able to use in my projects and ship updates for others using my apps. I wasn’t sure if it affected everyone but I couldn’t take a chance.

I’ll paste the test program below, I can’t attach `.cr` file. It turns out it’s not just me. A colleague hadn’t updated yet, so he ran the test _before_ and _after_ and boom. So I am now looking for more data. **Anyone who is willing to run the program, instructions are in the comment of the program, would be greatly appreciated.** 🙏

If you have the same problem you will get the following verdict:

```auto
== Verdict
  FAIL closed port raises
  FAIL localhost falls back
  FAIL 2nd connect reports refusal
  AFFECTED: Crystal does not report refused connections on this system.

```

If you don’t have the problem you will get this:

```auto
== Verdict
  ok closed port raises
  ok localhost falls back
  ok 2nd connect reports refusal
  NOT AFFECTED

```

Here’s the test script to save and run.

```crystal
# Checks whether Crystal's Socket#connect misses refused connections, as seen
# on macOS 26.7. Standalone: does not use socket_connect_fix and needs no
# running server.
#
# Usage:
# crystal run connect_refusal_check_v2.cr
# crystal run -Devloop=libevent connect_refusal_check_v2.cr
#
# Check 1 decides the verdict. Check 2 shows the user-visible symptom and needs
# localhost to resolve another address before 127.0.0.1. Check 3 shows what the
# kernel returns for a non-blocking connect, and check 4 the same for a
# blocking one, both independent of Crystal. Exits 1 if affected, 0 if not, 2 if
# check 1 could not run.
require "socket"

{% if flag?(:darwin) || flag?(:bsd) %}
  SO_ERROR = 0x1007
{% elsif flag?(:linux) %}
  SO_ERROR = 4
{% else %}
  {% raise "SO_ERROR constant unknown for this platform" %}
{% end %}

# Returns trimmed stdout of *cmd*, or "n/a" if it cannot run.
def command_output(cmd : String, args : Array(String)) : String
  io = IO::Memory.new
  status = Process.run(cmd, args, output: io, error: Process::Redirect::Close)
  status.success? ? io.to_s.strip.gsub('\n', ' ') : "n/a"
rescue
  "n/a"
end

# Returns the kernel's pending error on *fd* and clears it.
def pending_error(fd) : Int32
  value = 0
  len = LibC::SocklenT.new(sizeof(Int32))
  LibC.getsockopt(fd, LibC::SOL_SOCKET, SO_ERROR, pointerof(value), pointerof(len))
  value
end

# Returns a port on *host* that nothing is listening on.
def closed_port(host : String) : Int32
  server = TCPServer.new(host, 0)
  port = server.local_address.port
  server.close
  port
end

def errno_text(n : Int32) : String
  n == 0 ? "0" : "#{n} #{Errno.new(n)}"
end

results = {} of String => Bool?

puts "== Environment"
puts " macOS #{command_output("sw_vers", ["-productVersion"])} (#{command_output("sw_vers", ["-buildVersion"])})"
puts " kernel #{command_output("uname", ["-sr"])}"
puts " arch #{command_output("uname", ["-m"])}"
puts " crystal #{Crystal::VERSION}"
puts " evloop #{Crystal::EventLoop.backend_class}"

puts "\n== Check 1: Socket#connect to a closed 127.0.0.1 port"
begin
  port = closed_port("127.0.0.1")
  sock = TCPSocket.new(Socket::Family::INET)
  begin
    sock.connect(Socket::IPAddress.new("127.0.0.1", port))
    err = pending_error(sock.fd)
    puts " connect returned success; SO_ERROR=#{errno_text(err)}"
    results["closed port raises"] = false
  rescue ex : Socket::ConnectError
    puts " connect raised #{ex.class}: #{ex.message}"
    results["closed port raises"] = true
  ensure
    sock.close
  end
rescue ex
  puts " could not run: #{ex.class}: #{ex.message}"
  results["closed port raises"] = nil
end

puts "\n== Check 2: TCPSocket.new(\"localhost\") with an IPv4-only server"
begin
  order = Socket::Addrinfo.tcp("localhost", 80).map(&.ip_address.address)
  puts " localhost resolves to #{order.join(", ")}"
  if order.first? == "127.0.0.1" || !order.includes?("127.0.0.1")
    puts " skipped: needs another address before 127.0.0.1"
    results["localhost falls back"] = nil
  else
    server = TCPServer.new("127.0.0.1", 0)
    begin
      client = TCPSocket.new("localhost", server.local_address.port, connect_timeout: 3)
      begin
        peer = client.remote_address.address
        puts " connected to #{peer}"
        results["localhost falls back"] = true
      rescue ex
        puts " returned a socket that fails on use: #{ex.class}: #{ex.message}"
        results["localhost falls back"] = false
      ensure
        client.close
      end
    rescue ex
      puts " raised #{ex.class}: #{ex.message}"
      results["localhost falls back"] = false
    ensure
      server.close
    end
  end
rescue ex
  puts " could not run: #{ex.class}: #{ex.message}"
  results["localhost falls back"] = nil
end

puts "\n== Check 3: raw libc non-blocking connect trace (no Crystal event loop)"
begin
  port = closed_port("127.0.0.1")
  addr = Socket::IPAddress.new("127.0.0.1", port)
  fd = LibC.socket(LibC::AF_INET, LibC::SOCK_STREAM, 0)
  raise "socket() failed: #{Errno.value}" if fd == -1
  LibC.fcntl(fd, LibC::F_SETFL, LibC.fcntl(fd, LibC::F_GETFL, 0) | LibC::O_NONBLOCK)

  ret1 = LibC.connect(fd, addr.to_unsafe, addr.size)
  e1 = ret1 == -1 ? Errno.value.value : 0
  sleep 100.milliseconds
  ret2 = LibC.connect(fd, addr.to_unsafe, addr.size)
  e2 = ret2 == -1 ? Errno.value.value : 0
  so = pending_error(fd)
  LibC.close(fd)

  puts " 1st connect() = #{ret1}, errno #{errno_text(e1)}"
  puts " 2nd connect() = #{ret2}, errno #{errno_text(e2)}"
  puts " SO_ERROR after = #{errno_text(so)}"
  results["2nd connect reports refusal"] = ret1 == -1 && e1 == Errno::ECONNREFUSED.value || e2 == Errno::ECONNREFUSED.value
rescue ex
  puts " could not run: #{ex.class}: #{ex.message}"
  results["2nd connect reports refusal"] = nil
end

puts "\n== Check 4: raw libc blocking connect (no O_NONBLOCK)"
begin
  port = closed_port("127.0.0.1")
  addr = Socket::IPAddress.new("127.0.0.1", port)
  fd = LibC.socket(LibC::AF_INET, LibC::SOCK_STREAM, 0)
  raise "socket() failed: #{Errno.value}" if fd == -1

  ret = 0
  errno = 0
  elapsed = Time.measure do
    ret = LibC.connect(fd, addr.to_unsafe, addr.size)
    errno = ret == -1 ? Errno.value.value : 0
  end
  so = pending_error(fd)
  LibC.close(fd)

  puts " connect() = #{ret}, errno #{errno_text(errno)}, after #{elapsed.total_milliseconds.round(1)} ms"
  puts " SO_ERROR after = #{errno_text(so)}"
  results["blocking connect reports refusal"] = errno == Errno::ECONNREFUSED.value
rescue ex
  puts " could not run: #{ex.class}: #{ex.message}"
  results["blocking connect reports refusal"] = nil
end

puts "\n== Verdict"
results.each do |name, ok|
  mark = ok.nil? ? "SKIP" : (ok ? "ok " : "FAIL")
  puts " #{mark} #{name}"
end

case results["closed port raises"]
when false
  puts " AFFECTED: Crystal does not report refused connections on this system."
  exit 1
when true
  puts " NOT AFFECTED"
  exit 0
else
  puts " INCONCLUSIVE: check 1 could not run."
  exit 2
end

```

Cheers.

PS. The GitHub repo with the workaround/fix shard is here: [GitHub - nogginly/socket\_connect\_fix.cr: Monkey-patched workaround for when Crystal's Socket#connect reports a refused connection as successful. · GitHub](https://github.com/nogginly/socket_connect_fix.cr)

---

<div class="post-metadata">

**Author:** ![e4pjacob](https://avatars.discourse-cdn.com/v4/letter/e/5daacb/32.png) [@e4pjacob](https://forum.crystal-lang.org/u/e4pjacob)\
**Post date:** [September 18, 2026, 2:51am UTC](https://forum.crystal-lang.org/t/socket-connection-error-after-macos-26-7-update/9145/2 "2026-09-18T02:51:41Z")

</div>

Some results from running your test program on a mac mini M1.

Before upgrading.

```auto
% crystal run test_socket.cr 
== Environment
  macOS 26.6.2 (25G83)
  kernel Darwin 25.6.0
  arch arm64
  crystal 1.21.0
  evloop Crystal::EventLoop::Kqueue

== Check 1: Socket#connect to a closed 127.0.0.1 port
  connect raised Socket::ConnectError: connect: Connection refused

== Check 2: TCPSocket.new("localhost") with an IPv4-only server
  localhost resolves to ::1, 127.0.0.1
  connected to 127.0.0.1

== Check 3: raw libc non-blocking connect trace (no Crystal event loop)
  1st connect() = -1, errno 36 EINPROGRESS
  2nd connect() = -1, errno 61 ECONNREFUSED
  SO_ERROR after = 0

== Verdict
  ok closed port raises
  ok localhost falls back
  ok 2nd connect reports refusal
  NOT AFFECTED

```

After upgrading macOS.

```auto
ld: warning: object file (/Users/peterj/.cache/crystal/Volumes-TOSHIBA EXT-work-play-crystal-test-socket-test_socket.cr/L-ibU-nwind5858R-easonC-ode.o0.o) was built for newer 'macOS' version (28.0) than being linked (27.0)
ld: warning: object file (/Users/peterj/.cache/crystal/Volumes-TOSHIBA EXT-work-play-crystal-test-socket-test_socket.cr/T-uple40S-tring44-ef230bc2db84fb8ce6dcaa21e11a1560.o0.o) was built for newer 'macOS' version (28.0) than being linked (27.0)
ld: warning: object file (/Users/peterj/.cache/crystal/Volumes-TOSHIBA EXT-work-play-crystal-test-socket-test_socket.cr/L-ibG-C-5858S-tackB-ase.o0.o) was built for newer 'macOS' version (28.0) than being linked (27.0)
ld: warning: object file (/Users/peterj/.cache/crystal/Volumes-TOSHIBA EXT-work-play-crystal-test-socket-test_socket.cr/T-uple40U-I-nt6441.o0.o) was built for newer 'macOS' version (28.0) than being linked (27.0)
== Environment
  macOS 27.0 (26A428)
  kernel Darwin 27.0.0
  arch arm64
  crystal 1.21.0
  evloop Crystal::EventLoop::Kqueue

== Check 1: Socket#connect to a closed 127.0.0.1 port
  connect returned success; SO_ERROR=61 ECONNREFUSED

== Check 2: TCPSocket.new("localhost") with an IPv4-only server
  localhost resolves to ::1, 127.0.0.1
  returned a socket that fails on use: Socket::Error: getpeername: Invalid argument

== Check 3: raw libc non-blocking connect trace (no Crystal event loop)
  1st connect() = -1, errno 36 EINPROGRESS
  2nd connect() = -1, errno 56 EISCONN
  SO_ERROR after = 61 ECONNREFUSED

== Verdict
  FAIL closed port raises
  FAIL localhost falls back
  FAIL 2nd connect reports refusal
  AFFECTED: Crystal does not report refused connections on this system.

```

```auto
ld: warning: object file (/Users/peterj/.cache/crystal/Volumes-TOSHIBA EXT-work-play-crystal-test-socket-test_socket.cr/L-ibU-nwind5858R-easonC-ode.o0.o) was built for newer 'macOS' version (28.0) than being linked (27.0)
ld: warning: object file (/Users/peterj/.cache/crystal/Volumes-TOSHIBA EXT-work-play-crystal-test-socket-test_socket.cr/T-uple40S-tring44-ef230bc2db84fb8ce6dcaa21e11a1560.o0.o) was built for newer 'macOS' version (28.0) than being linked (27.0)
ld: warning: object file (/Users/peterj/.cache/crystal/Volumes-TOSHIBA EXT-work-play-crystal-test-socket-test_socket.cr/L-ibG-C-5858S-tackB-ase.o0.o) was built for newer 'macOS' version (28.0) than being linked (27.0)
ld: warning: object file (/Users/peterj/.cache/crystal/Volumes-TOSHIBA EXT-work-play-crystal-test-socket-test_socket.cr/T-uple40U-I-nt6441.o0.o) was built for newer 'macOS' version (28.0) than being linked (27.0)
== Environment
  macOS 27.0 (26A428)
  kernel Darwin 27.0.0
  arch arm64
  crystal 1.21.0
  evloop Crystal::EventLoop::LibEvent

== Check 1: Socket#connect to a closed 127.0.0.1 port
  connect returned success; SO_ERROR=61 ECONNREFUSED

== Check 2: TCPSocket.new("localhost") with an IPv4-only server
  localhost resolves to ::1, 127.0.0.1
  returned a socket that fails on use: Socket::Error: getpeername: Invalid argument

== Check 3: raw libc non-blocking connect trace (no Crystal event loop)
  1st connect() = -1, errno 36 EINPROGRESS
  2nd connect() = -1, errno 56 EISCONN
  SO_ERROR after = 61 ECONNREFUSED

== Verdict
  FAIL closed port raises
  FAIL localhost falls back
  FAIL 2nd connect reports refusal
  AFFECTED: Crystal does not report refused connections on this system.

```

So it behaved as you described but with a truck load of loader warnings, most of which I have omitted from the transcripts.

Hope that you find this useful.

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [September 18, 2026, 5:50am UTC](https://forum.crystal-lang.org/t/socket-connection-error-after-macos-26-7-update/9145/3 "2026-09-18T05:50:19Z")

</div>

Could you test the raw libc behaviour without `O_NONBLOCK` to get a full picture?

Also I understand the behaviour with kqueue event loop (default) and libevent are identical (works before 26.7, broken after). Is that correct?

---

<div class="post-metadata">

**Author:** ![e4pjacob](https://avatars.discourse-cdn.com/v4/letter/e/5daacb/32.png) [@e4pjacob](https://forum.crystal-lang.org/u/e4pjacob)\
**Post date:** [September 18, 2026, 6:21am UTC](https://forum.crystal-lang.org/t/socket-connection-error-after-macos-26-7-update/9145/4 "2026-09-18T06:21:24Z")

</div>

Here’s the console transcript for 27.0 with O\_NONBLOCK removed.

```auto
ld: warning: object file (/Users/peterj/.cache/crystal/Volumes-TOSHIBA EXT-work-play-crystal-test-socket-test_socket_2.cr/L-ibG-C-5858S-tackB-ase.o0.o) was built for newer 'macOS' version (28.0) than being linked (27.0)
ld: warning: object file (/Users/peterj/.cache/crystal/Volumes-TOSHIBA EXT-work-play-crystal-test-socket-test_socket_2.cr/T-uple40U-I-nt6441.o0.o) was built for newer 'macOS' version (28.0) than being linked (27.0)
== Environment
  macOS 27.0 (26A428)
  kernel Darwin 27.0.0
  arch arm64
  crystal 1.21.0
  evloop Crystal::EventLoop::Kqueue

== Check 1: Socket#connect to a closed 127.0.0.1 port
  connect returned success; SO_ERROR=61 ECONNREFUSED

== Check 2: TCPSocket.new("localhost") with an IPv4-only server
  localhost resolves to ::1, 127.0.0.1
  returned a socket that fails on use: Socket::Error: getpeername: Invalid argument

== Check 3: raw libc connect trace (no Crystal event loop)
  1st connect() = -1, errno 61 ECONNREFUSED
  2nd connect() = -1, errno 56 EISCONN
  SO_ERROR after = 0

== Verdict
  FAIL closed port raises
  FAIL localhost falls back
  ok 2nd connect reports refusal
  AFFECTED: Crystal does not report refused connections on this system.

```

I no longer have a 26.7 system so I cannot answer your other question.

---

<div class="post-metadata">

**Author:** ![nogginly](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nogginly/32/1795_2.png) [@nogginly](https://forum.crystal-lang.org/u/nogginly)\
**Post date:** [September 18, 2026, 11:59am UTC](https://forum.crystal-lang.org/t/socket-connection-error-after-macos-26-7-update/9145/5 "2026-09-18T11:59:07Z")

</div>

Thank you. Looks like it’s not me or my colleagues with this issue (since we share a lot of common compliance-related tools on our Macs).

---

<div class="post-metadata">

**Author:** ![nogginly](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nogginly/32/1795_2.png) [@nogginly](https://forum.crystal-lang.org/u/nogginly)\
**Post date:** [September 18, 2026, 12:01pm UTC](https://forum.crystal-lang.org/t/socket-connection-error-after-macos-26-7-update/9145/6 "2026-09-18T12:01:27Z")

</div>

1. Done, I’ve updated the topic post with v2 of script. Good news with that:

```auto
== Verdict
  FAIL closed port raises
  FAIL localhost falls back
  FAIL 2nd connect reports refusal
  ok blocking connect reports refusal
  AFFECTED: Crystal does not report refused connections on this system.

```

1. Correct, same OK and FAIL with both types before and after update respectively.

---

<div class="post-metadata">

**Author:** ![nogginly](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nogginly/32/1795_2.png) [@nogginly](https://forum.crystal-lang.org/u/nogginly)\
**Post date:** [September 18, 2026, 12:23pm UTC](https://forum.crystal-lang.org/t/socket-connection-error-after-macos-26-7-update/9145/7 "2026-09-18T12:23:59Z")

</div>

> 2026-09-18 - Results so far. Tables produced by AI and verified by human (moi 😁).

**Analysis A, machine by version band** (empty cells are untested):

| Machine | ≤ 26.6.2 | ≥ 26.7 |
| --- | --- | --- |
| Yours (arm64) | | Affected |
| Colleague (arm64) | Not affected (15) | Affected (26.7) |
| `e4pjacob` (arm64) | Not affected (26.6.2) | Affected (27.0 beta) |
| Linux 6.18 (x86\_64) | Not affected | Not affected |

**Analysis B, observation by version band** , if you’d rather emphasise the mechanism:

| Observation | ≤ 26.6.2 | ≥ 26.7 |
| --- | --- | --- |
| Blocking `connect()` | `ECONNREFUSED` | `ECONNREFUSED` |
| 2nd non-blocking `connect()` | `ECONNREFUSED` | `EISCONN` |
| `SO_ERROR` afterwards | 0, error consumed | 61, `ECONNREFUSED` |
| `Socket#connect` | Raises | Returns success |
| `localhost` fallback | Works | Stops at `::1` |

---

<div class="post-metadata">

**Author:** ![ysbaddaden](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/ysbaddaden/32/2252_2.png) [@ysbaddaden](https://forum.crystal-lang.org/u/ysbaddaden)\
**Post date:** [September 18, 2026, 12:58pm UTC](https://forum.crystal-lang.org/t/socket-connection-error-after-macos-26-7-update/9145/8 "2026-09-18T12:58:41Z")

</div>

Can you check if [Fix: Socket#connect on macOS 26.7 by ysbaddaden · Pull Request #17436 · crystal-lang/crystal · GitHub](https://github.com/crystal-lang/crystal/pull/17436) fixes the issue? That’s basically your monkey-patch, applied to the evloops and limited to macOS.

---

<div class="post-metadata">

**Author:** ![ysbaddaden](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/ysbaddaden/32/2252_2.png) [@ysbaddaden](https://forum.crystal-lang.org/u/ysbaddaden)\
**Post date:** [September 18, 2026, 4:18pm UTC](https://forum.crystal-lang.org/t/socket-connection-error-after-macos-26-7-update/9145/9 "2026-09-18T16:18:39Z")

</div>

Oh, @straight-shoota realized that the nonblocking loop was just wrong.

I updated the PR with the corrected loop. Hopefully it works on macOS 26.7 @noggingly ?

---

<div class="post-metadata">

**Author:** ![nogginly](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nogginly/32/1795_2.png) [@nogginly](https://forum.crystal-lang.org/u/nogginly)\
**Post date:** [September 18, 2026, 5:33pm UTC](https://forum.crystal-lang.org/t/socket-connection-error-after-macos-26-7-update/9145/10 "2026-09-18T17:33:18Z")

</div>

Thanks for [submitting PR](https://github.com/crystal-lang/crystal/pull/17436) for the socket connection issue that I ran into. 🙏

I built Crystal from source from your branch and ran it against the v2 script.

```auto
== Environment
  macOS 26.7 (25G229)
  kernel Darwin 25.6.0
  arch arm64
  crystal 1.22.0-dev
  evloop Crystal::EventLoop::Kqueue

== Check 1: Socket#connect to a closed 127.0.0.1 port
  connect raised Socket::ConnectError: connect: Operation now in progress

== Check 2: TCPSocket.new("localhost") with an IPv4-only server
  localhost resolves to ::1, 127.0.0.1
  connected to 127.0.0.1

== Check 3: raw libc non-blocking connect trace (no Crystal event loop)
  1st connect() = -1, errno 36 EINPROGRESS
  2nd connect() = -1, errno 56 EISCONN
  SO_ERROR after = 61 ECONNREFUSED

== Check 4: raw libc blocking connect (no O_NONBLOCK)
  connect() = -1, errno 61 ECONNREFUSED, after 0.1 ms
  SO_ERROR after = 0

== Verdict
  ok closed port raises
  ok localhost falls back
  FAIL 2nd connect reports refusal
  ok blocking connect reports refusal
  NOT AFFECTED

```

For comparison below is the result from running same script using the Crystal 1.21 release.

```auto
== Environment
  macOS 26.7 (25G229)
  kernel Darwin 25.6.0
  arch arm64
  crystal 1.21.0
  evloop Crystal::EventLoop::Kqueue

== Check 1: Socket#connect to a closed 127.0.0.1 port
  connect returned success; SO_ERROR=61 ECONNREFUSED

== Check 2: TCPSocket.new("localhost") with an IPv4-only server
  localhost resolves to ::1, 127.0.0.1
  returned a socket that fails on use: Socket::Error: getpeername: Invalid argument

== Check 3: raw libc non-blocking connect trace (no Crystal event loop)
  1st connect() = -1, errno 36 EINPROGRESS
  2nd connect() = -1, errno 56 EISCONN
  SO_ERROR after = 61 ECONNREFUSED

== Check 4: raw libc blocking connect (no O_NONBLOCK)
  connect() = -1, errno 61 ECONNREFUSED, after 0.2 ms
  SO_ERROR after = 0

== Verdict
  FAIL closed port raises
  FAIL localhost falls back
  FAIL 2nd connect reports refusal
  ok blocking connect reports refusal
  AFFECTED: Crystal does not report refused connections on this system.

```

As a comparison here’s the result from BEFORE the update (from my colleague’s macOS 26.6.2). For “Check 1” the exception after the fix is different from back when it used to work. But I don’t know enough to tell you one is more right than the other, so consider this FY consideration only.

```auto
== Environment
  macOS 26.6.2 (25G83)
  kernel Darwin 25.6.0
  arch arm64
  crystal 1.21.0
  evloop Crystal::EventLoop::Kqueue

== Check 1: Socket#connect to a closed 127.0.0.1 port
  connect raised Socket::ConnectError: connect: Connection refused

== Check 2: TCPSocket.new("localhost") with an IPv4-only server
  localhost resolves to 127.0.0.1, ::1
  skipped: needs another address before 127.0.0.1

== Check 3: raw libc non-blocking connect trace (no Crystal event loop)
  1st connect() = -1, errno 36 EINPROGRESS
  2nd connect() = -1, errno 61 ECONNREFUSED
  SO_ERROR after = 0

== Verdict
  ok closed port raises
  SKIP localhost falls back
  ok 2nd connect reports refusal
  NOT AFFECTED

```

Cheers.

---

<div class="post-metadata">

**Author:** ![nogginly](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nogginly/32/1795_2.png) [@nogginly](https://forum.crystal-lang.org/u/nogginly)\
**Post date:** [September 27, 2026, 5:49pm UTC](https://forum.crystal-lang.org/t/socket-connection-error-after-macos-26-7-update/9145/11 "2026-09-27T17:49:04Z")

</div>

Thanks @ysbaddaden and @straight-shoota for hearing me out and getting a fix put through.

> [@Crystal 1.21.1 is released!](https://forum.crystal-lang.org/t/crystal-1-21-1-is-released/9171):
>
