Problem
When the underlying gRPC channel becomes unusable (half-open / silently dropped), RPCs appear to hang until the caller's own timeout rather than failing fast. There is currently no way to configure grpc.keepalive_time_ms / keepalive_timeout_ms / keepalive_permit_without_calls, because ClientOptions does not accept channel options and loadService() constructs the service with only sslCreds:
const client = new protoService({ sslCreds: this.getCredentials() });
ClientOptions is identical on 23.0.0 and 24.1.0:
{ client_id, client_secret, developer_token, disable_parsing?, max_reporting_rows? }
Why the existing service cache doesn't appear to mitigate this
serviceCache has a 10-minute TTL with a dispose that closes the service, which looks like it should self-heal. In our environment it did not. @grpc/grpc-js maintains a global subchannel pool keyed by target and channel args, so a rebuilt service can draw the same underlying connection. We saw calls 29 minutes apart both hang — i.e. after the cached service should already have been evicted and recreated. Only a process restart restored normal behaviour.
To be clear about the limits of this evidence: we did not capture a socket-level trace, so the subchannel-pool explanation is inferred from observed behaviour rather than directly proven. What we can state confidently is that the condition was process-scoped and survived the service cache TTL.
Impact
Offline conversion uploads hang for the full caller timeout and return no error, so callers cannot distinguish "connection is unusable" from "the API is slow", and cannot recover in-process. Because there is no fast failure signal, this went undetected across multiple days over roughly a month in our case.
Evidence that the fault is transport-level rather than credentials or API-side
Captured during an active occurrence, same account and same credentials:
| Path |
Result |
gRPC uploadClickConversions (this SDK) |
hung >60s, repeatedly |
| OAuth token refresh (REST) |
HTTP 200 in 0.13s |
googleAds:search v25 (REST) |
HTTP 200 in 0.34s |
| Same rows retried after a process restart |
uploaded immediately |
Other outbound integrations in the same process and same time window (all REST/HTTP1) were unaffected.
Requested change
Allow channel options to be passed through to the service constructor — for example an optional channel_options on ClientOptions, forwarded into:
new protoService({ sslCreds: this.getCredentials(), ...channelOptions });
Including those options in the serviceCache key would keep differing configurations isolated from one another.
Deliberately scoped to that passthrough and the cache-key change — no change to defaults or existing behaviour for callers who don't opt in. Happy to open a PR if you'd like it shaped a particular way.
Environment
google-ads-api 23.0.0 (surface also verified against 24.1.0)
@grpc/grpc-js 1.14.4
google-gax 5.0.6
- Node, running on Railway
Problem
When the underlying gRPC channel becomes unusable (half-open / silently dropped), RPCs appear to hang until the caller's own timeout rather than failing fast. There is currently no way to configure
grpc.keepalive_time_ms/keepalive_timeout_ms/keepalive_permit_without_calls, becauseClientOptionsdoes not accept channel options andloadService()constructs the service with onlysslCreds:ClientOptionsis identical on 23.0.0 and 24.1.0:Why the existing service cache doesn't appear to mitigate this
serviceCachehas a 10-minute TTL with adisposethat closes the service, which looks like it should self-heal. In our environment it did not.@grpc/grpc-jsmaintains a global subchannel pool keyed by target and channel args, so a rebuilt service can draw the same underlying connection. We saw calls 29 minutes apart both hang — i.e. after the cached service should already have been evicted and recreated. Only a process restart restored normal behaviour.To be clear about the limits of this evidence: we did not capture a socket-level trace, so the subchannel-pool explanation is inferred from observed behaviour rather than directly proven. What we can state confidently is that the condition was process-scoped and survived the service cache TTL.
Impact
Offline conversion uploads hang for the full caller timeout and return no error, so callers cannot distinguish "connection is unusable" from "the API is slow", and cannot recover in-process. Because there is no fast failure signal, this went undetected across multiple days over roughly a month in our case.
Evidence that the fault is transport-level rather than credentials or API-side
Captured during an active occurrence, same account and same credentials:
uploadClickConversions(this SDK)googleAds:searchv25 (REST)Other outbound integrations in the same process and same time window (all REST/HTTP1) were unaffected.
Requested change
Allow channel options to be passed through to the service constructor — for example an optional
channel_optionsonClientOptions, forwarded into:Including those options in the
serviceCachekey would keep differing configurations isolated from one another.Deliberately scoped to that passthrough and the cache-key change — no change to defaults or existing behaviour for callers who don't opt in. Happy to open a PR if you'd like it shaped a particular way.
Environment
google-ads-api23.0.0 (surface also verified against 24.1.0)@grpc/grpc-js1.14.4google-gax5.0.6