Skip to content

fix(flags): do not retry a refused flags connection - #1180

Merged
marandaneto merged 4 commits into
PostHog:mainfrom
luantaraschi:fix/flags-no-retry-conn-refused
Nov 6, 2026
Merged

marandaneto merged 4 commits into
PostHog:mainfrom
luantaraschi:fix/flags-no-retry-conn-refused

Conversation

@luantaraschi

@luantaraschi luantaraschi commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

flags() retries on any requests.exceptions.ConnectionError. requests raises that same class for a refused connection (ConnectionRefusedError wrapped in urllib3's NewConnectionError and MaxRetryError), so when nothing is listening on the host, the request is sent again after 300 ms (and again after 600 ms with max_retries=2) before the error reaches the caller.

The HTTP client spec in PostHog/sdk-specs treats a refused connection as non-transient: the SDK should not retry it and should surface the flag error right away (acceptance/private/http-client.feature, "exactly 1 feature flag request should be sent"). compliance/posthog-python.md lists this as gap n18.

This change looks for a ConnectionRefusedError behind the transport error before retrying, without following unrelated exceptions the application was handling, and re-raises when it finds one. Timeouts, resets, DNS and TLS failures and 502/504 responses are still retried.

💚 How did you test it?

  • New test_flags_does_not_retry_refused_connection binds and closes a local socket, then calls flags() against that port with max_retries=2. On main it sends 3 requests; with the fix it sends 1 and never sleeps. The existing retry tests still pass.
  • ruff format --check and ruff check (0.12.2) are clean.
  • Full suite in a python:3.12-slim container: 6052 passed, no failures.

📝 Checklist

  • All commits are signed (see Commit signing)
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran sampo add to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Claude Code with Claude Opus 5.5 found this while checking posthog-python against the HTTP client requirements in PostHog/sdk-specs, reproduced it against a closed local port, and wrote the fix and the test. Luan directed the work.

A refused connection is not transient, so flags() now raises it on the first attempt instead of retrying it on the backoff schedule. Timeouts, resets, DNS and TLS failures are still retried.
@luantaraschi
luantaraschi requested a review from a team as a code owner November 4, 2026 15:37

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated advisory code review.

Comment thread posthog/request.py Resolved
@marandaneto
marandaneto requested a review from a team November 5, 2026 12:22
luantaraschi and others added 2 commits November 5, 2026 13:10
A timeout raised while the application handles its own refused connection was treated as a refusal and not retried.
@marandaneto

Copy link
Copy Markdown
Member

thanks @luantaraschi

@marandaneto
marandaneto enabled auto-merge (squash) November 6, 2026 08:06
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

SDK performance: posthog-python

Benchmarks completed. No changes flagged for review.

Flags require at least a 5% change in the median and all six pairs agreeing on direction. This is a noise filter, not proof of a performance change or a CI gate.

View all results
Test Measurement Before PR With PR Change Assessment
Capture and deliver events (32-byte payload, burst)
Shard: capture
Managed heap after settling — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​; head:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Managed heap after settling — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​; head:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​
Capture and deliver events (32-byte payload, steady)
Shard: capture
Managed heap after settling — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​; head:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Managed heap after settling — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​; head:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​
Queue an event (32-byte payload)
Shard: capture
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​; head:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​
Queue an event (32-byte payload)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​; head:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​
Queue an event (4,096-byte payload)
Shard: capture
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​; head:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​
Queue an event (4,096-byte payload)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​; head:​ Background preparation has no public completion boundary before flush; see capture.​drain for full-pipeline allocations.​
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Managed heap after settling — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​; head:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​
Refresh 1,000 flag definitions
Shard: flags-memory
Managed heap after settling — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​; head:​ CPython has no uninstrumented total managed-heap byte counter; tracing would distort this RSS pass.​
Capture and deliver events (32-byte payload, burst)
Shard: capture
Allocated memory per operation 113.54 KiB/op 113.54 KiB/op <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Allocations per operation 1,096.58 count/op 1,096.59 count/op <0.1% more Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Time per operation 114.08 µs/op 114.14 µs/op <0.1% more time Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Idle process memory (RSS) 36.03 MiB 36.01 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Observed peak process memory (RSS) 37.79 MiB 37.76 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Process memory after settling (RSS) 37.79 MiB 37.76 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Process CPU time per operation 114.65 µs/op 114.95 µs/op 0.3% more time Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Lifetime peak process memory (RSS) 40.32 MiB 40.32 MiB No observed change Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Allocated memory per operation 166.1 KiB/op 166.1 KiB/op <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Allocations per operation 1,089.63 count/op 1,089.63 count/op <0.1% fewer Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Time per operation 148.02 µs/op 145.66 µs/op 1.6% less time Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Idle process memory (RSS) 40.86 MiB 40.77 MiB 0.2% less memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Observed peak process memory (RSS) 45.96 MiB 45.87 MiB 0.2% less memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Process memory after settling (RSS) 45.96 MiB 45.87 MiB 0.2% less memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Process CPU time per operation 141.03 µs/op 135.11 µs/op 4.2% less time Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Lifetime peak process memory (RSS) 50.74 MiB 50.76 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Allocated memory per operation 117.68 KiB/op 117.68 KiB/op <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Allocations per operation 1,136.62 count/op 1,136.59 count/op <0.1% fewer Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Time per operation 129.73 µs/op 132.4 µs/op 2.1% more time Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Idle process memory (RSS) 35.97 MiB 35.95 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Observed peak process memory (RSS) 36.07 MiB 36.07 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Process memory after settling (RSS) 36.07 MiB 36.07 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Process CPU time per operation 129.41 µs/op 132.24 µs/op 2.2% more time Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Lifetime peak process memory (RSS) 40.32 MiB 40.32 MiB No observed change Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Allocated memory per operation 170.57 KiB/op 170.57 KiB/op <0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Allocations per operation 1,129.47 count/op 1,129.5 count/op <0.1% more Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Time per operation 153.23 µs/op 150.56 µs/op 1.7% less time Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Idle process memory (RSS) 40.62 MiB 40.7 MiB 0.2% more memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Observed peak process memory (RSS) 40.62 MiB 40.71 MiB 0.2% more memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Process memory after settling (RSS) 40.62 MiB 40.71 MiB 0.2% more memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Process CPU time per operation 146.2 µs/op 143.56 µs/op 1.8% less time Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Lifetime peak process memory (RSS) 40.97 MiB 40.9 MiB 0.2% less memory Not flagged
Queue an event (32-byte payload)
Shard: capture
Caller-thread CPU time per operation 92.8 µs/op 81.95 µs/op 11.7% less time Not flagged
Queue an event (32-byte payload)
Shard: capture
Time per operation 98.17 µs/op 82.06 µs/op 16.4% less time Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Caller-thread CPU time per operation 82.58 µs/op 89.78 µs/op 8.7% more time Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Time per operation 88.93 µs/op 92.4 µs/op 3.9% more time Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Allocated memory per operation 200.98 KiB/op 200.98 KiB/op No observed change Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Allocations per operation 2,339.33 count/op 2,339.33 count/op No observed change Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Time per operation 64.22 µs/op 65.39 µs/op 1.8% more time Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Process CPU time per operation 64.23 µs/op 65.4 µs/op 1.8% more time Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Allocated memory per operation 1.93 MiB/op 1.93 MiB/op No observed change Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Allocations per operation 23,013.56 count/op 23,013.56 count/op No observed change Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Time per operation 638.61 µs/op 627.35 µs/op 1.8% less time Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Process CPU time per operation 638.66 µs/op 627.46 µs/op 1.8% less time Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Allocated memory per operation 19.24 MiB/op 19.24 MiB/op No observed change Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Allocations per operation 229,654.94 count/op 229,654.94 count/op No observed change Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Time per operation 7.22 ms/op 7.06 ms/op 2.3% less time Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Process CPU time per operation 7.22 ms/op 7.06 ms/op 2.3% less time Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Idle process memory (RSS) 41.69 MiB 41.7 MiB <0.1% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Observed peak process memory (RSS) 41.96 MiB 41.97 MiB <0.1% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Process memory after settling (RSS) 41.96 MiB 41.97 MiB <0.1% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Lifetime peak process memory (RSS) 42.44 MiB 42.48 MiB 0.1% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Idle process memory (RSS) 42.13 MiB 41.42 MiB 1.7% less memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Observed peak process memory (RSS) 42.18 MiB 42.11 MiB 0.2% less memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Process memory after settling (RSS) 42.18 MiB 41.5 MiB 1.6% less memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Lifetime peak process memory (RSS) 42.55 MiB 42.51 MiB 0.1% less memory Not flagged
Evaluate one boolean flag
Shard: flags-single
Allocated memory per operation 15.05 KiB/op 15.05 KiB/op No observed change Not flagged
Evaluate one boolean flag
Shard: flags-single
Allocations per operation 173.76 count/op 173.76 count/op No observed change Not flagged
Evaluate one boolean flag
Shard: flags-single
Time per operation 13.85 µs/op 13.86 µs/op <0.1% more time Not flagged
Evaluate one boolean flag
Shard: flags-single
Process CPU time per operation 13.85 µs/op 13.86 µs/op <0.1% more time Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Allocated memory per operation 23.12 KiB/op 23.12 KiB/op No observed change Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Allocations per operation 270.27 count/op 270.27 count/op No observed change Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Time per operation 21.51 µs/op 21.46 µs/op 0.2% less time Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Process CPU time per operation 21.51 µs/op 21.46 µs/op 0.2% less time Not flagged
Evaluate a flag (early match)
Shard: flags-single
Allocated memory per operation 35.71 KiB/op 35.71 KiB/op No observed change Not flagged
Evaluate a flag (early match)
Shard: flags-single
Allocations per operation 387.27 count/op 387.27 count/op No observed change Not flagged
Evaluate a flag (early match)
Shard: flags-single
Time per operation 27.68 µs/op 27.39 µs/op 1% less time Not flagged
Evaluate a flag (early match)
Shard: flags-single
Process CPU time per operation 27.67 µs/op 27.38 µs/op 1% less time Not flagged
Evaluate a flag (late match)
Shard: flags-single
Allocated memory per operation 53.59 KiB/op 53.59 KiB/op No observed change Not flagged
Evaluate a flag (late match)
Shard: flags-single
Allocations per operation 576.27 count/op 576.27 count/op No observed change Not flagged
Evaluate a flag (late match)
Shard: flags-single
Time per operation 38.1 µs/op 38.03 µs/op 0.2% less time Not flagged
Evaluate a flag (late match)
Shard: flags-single
Process CPU time per operation 38.1 µs/op 38.03 µs/op 0.2% less time Not flagged
Evaluate a flag (no match)
Shard: flags-single
Allocated memory per operation 52.14 KiB/op 52.14 KiB/op No observed change Not flagged
Evaluate a flag (no match)
Shard: flags-single
Allocations per operation 556.77 count/op 556.77 count/op No observed change Not flagged
Evaluate a flag (no match)
Shard: flags-single
Time per operation 34.68 µs/op 34.69 µs/op <0.1% more time Not flagged
Evaluate a flag (no match)
Shard: flags-single
Process CPU time per operation 34.68 µs/op 34.69 µs/op <0.1% more time Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Allocated memory per operation 17.84 KiB/op 17.84 KiB/op No observed change Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Allocations per operation 198.27 count/op 198.27 count/op No observed change Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Time per operation 16.29 µs/op 16.31 µs/op 0.1% more time Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Process CPU time per operation 16.29 µs/op 16.31 µs/op 0.1% more time Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Allocated memory per operation 19.23 KiB/op 19.23 KiB/op No observed change Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Allocations per operation 210.77 count/op 210.77 count/op No observed change Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Time per operation 17.44 µs/op 17.64 µs/op 1.2% more time Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Process CPU time per operation 17.44 µs/op 17.64 µs/op 1.2% more time Not flagged
Evaluate a rollout flag
Shard: flags-single
Allocated memory per operation 14.04 KiB/op 14.04 KiB/op No observed change Not flagged
Evaluate a rollout flag
Shard: flags-single
Allocations per operation 160.76 count/op 160.76 count/op No observed change Not flagged
Evaluate a rollout flag
Shard: flags-single
Time per operation 12.97 µs/op 13.08 µs/op 0.8% more time Not flagged
Evaluate a rollout flag
Shard: flags-single
Process CPU time per operation 12.97 µs/op 13.08 µs/op 0.8% more time Not flagged
Evaluate a multivariate flag
Shard: flags-single
Allocated memory per operation 15.21 KiB/op 15.21 KiB/op No observed change Not flagged
Evaluate a multivariate flag
Shard: flags-single
Allocations per operation 177.77 count/op 177.77 count/op No observed change Not flagged
Evaluate a multivariate flag
Shard: flags-single
Time per operation 14.44 µs/op 14.56 µs/op 0.9% more time Not flagged
Evaluate a multivariate flag
Shard: flags-single
Process CPU time per operation 14.44 µs/op 14.56 µs/op 0.9% more time Not flagged

Values are medians of paired measurements. Not flagged does not mean unchanged. Unavailable means a measurement could not be provided, not zero. Estimated allocation measurements retain their instrumentation uncertainty. RSS measures the whole process, not just SDK allocations; observed peaks are sampled checkpoints. Paired ranges in the diagnostics describe variability, not confidence intervals.

Each shard uses its own runner; compare Before PR against With PR within a row, not across shards.

Compared 6b7c0ba3e72b8ebdc3b1f65aedafbb44c7c67256 → d73e4fdd3ef16f019664dcdb13101f211efc343d.

Full report and diagnostics

102 metric rows; 102 rows shown in all results. Omitted or shortened data remain in comparison.json, including per-runner environments, resource limits, measurement methods, samples and checkpoints.

@marandaneto
marandaneto merged commit 11585fb into PostHog:main Nov 6, 2026
49 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants