Skip to content

Concurrent request execution is exponentially slow #297

Closed

Description

@OutdatedGuy

Describe the bug
Concurrently executing thousands of queries is super slow in dart. For below code it took around ~2s for nodejs version to finish executing and around ~57s for dart version.

To Reproduce

The code should call production (actual firestore) APIs and not a local emulator to see the issue.

Dart (Super Slow):

import 'package:firebase_admin_sdk/firebase_admin_sdk.dart';
import 'package:firebase_functions/firebase_functions.dart';

void main() {
  runFunctions((firebase) {
    firebase.https.onRequest(name: 'helloWorld', (request) async {
      final arr = List.generate(3000, (index) => index);

      final firestore = FirebaseApp.instance.firestore();

      await Future.wait(
        arr.map((index) {
          return firestore
              .collection('numbers')
              .where('number', .equal, index)
              .limit(1)
              .get();
        }),
        eagerError: true,
      );

      return .ok('Yay! Client Exception did not occur.');
    });
  });
}

Typescript (Fast):

import {initializeApp} from "firebase-admin/app";
import {getFirestore} from "firebase-admin/firestore";
import {onRequest} from "firebase-functions/https";
initializeApp();

export const helloWorld = onRequest(async (_request, response) => {
  const arr = Array.from({length: 3000}, (_, index) => index);

  const firestore = getFirestore();

  await Promise.all(
    arr.map((index) => {
      return firestore
        .collection("numbers")
        .where("number", "==", index)
        .limit(1)
        .get();
    }),
  );

  response.status(200).send("Yay! Client Exception did not occur.");
});

Expected behavior
The speed of executing thousands of queries concurrently in dart should be as fast as if not faster than nodejs version.

Activity

  1. brianquinlan commented on Jul 6, 2026

    @brianquinlan
    Contributor

    @lahirumaramba wrote:

    I also believe the delay is caused by resource exhaustion with HTTP/1. the TypeScript version is likely faster because @google-cloud/firestore (Node.js) uses gRPC over HTTP/2 with multiplexing.

    The optimal solution would be to implement HTTP/2 multiplexing in Dart. Do we currently use HTTP/2 for Messaging? Maybe we can reuse some of that logic here.

    It also appears the current implementation of FirestoreHttpClient does not define any connection bounds or limits. The client may be attempting 3000 simultaneous connections and exhaust resources. Can we try setting a lower maximum connection threshold? This should ideally enable connection pooling and allow the same client to be used in batches @brian Quinlan WDYT?

    I don't know if we just HTTP/2 for messaging. Setting the maximum number of connections is very easy using maxConnectionsPerHost.

    Maybe tomorrow I'll try to repro and see if switching to an HTTP/3 implementation improves performance.

  2. mosuem commented on Jul 6, 2026

    @mosuem

    See also the AI-generated #298 for the max connections - @demolaf is investigating in more detail though.

  3. brianquinlan commented on Jul 7, 2026

    @brianquinlan
    Contributor

    I wasn't able to reproduce this on my MacBook until I introduced 50ms of network delay.

  4. brianquinlan commented on Jul 7, 2026

    @brianquinlan
    Contributor

    Here are the results that I get on my macOS machine:

    Metric IOClient IOClient (maxConnections=5) CupertinoClient
    200 Queries 1,243 ms 3,717 ms N/A
    1000 Queries 2,112 ms 14,221 ms ~2,400 ms
    3000 Queries 4,470 ms 39,705 ms (39.7s) 4,644 ms (4.6s)

  5. demolaf commented on Jul 8, 2026

    @demolaf
    Member

    @brianquinlan he's piping through cloud functions, so maybe a lack of resources on the platform (Cloud Run)? My deployed function at 3000 queries produced the below (by default):

    void main(List<String> args) async {
      FirebaseApp.initializeApp();
    
      await runFunctions((firebase) {
        firebase.https.onRequest(
          name: 'issue297Repro',
          options: const HttpsOptions(invoker: Invoker.public()),
          (request) async {
            final concurrency =
                int.tryParse(request.url.queryParameters['n'] ?? '') ?? 100;
    
            final firestore = firebase.adminApp.firestore();
            final collection = firestore.collection('repro297_numbers');
    
            final stopwatch = Stopwatch()..start();
            var errors = 0;
    
            await Future.wait(
              List.generate(concurrency, (index) async {
                try {
                  await collection
                      .where('number', WhereFilter.equal, index)
                      .limit(1)
                      .get();
                } catch (_) {
                  errors++;
                }
              }),
              eagerError: false,
            );
    
            stopwatch.stop();
    
            return Response.ok(
              'concurrency=$concurrency elapsed=${stopwatch.elapsed} errors=$errors\n',
            );
          },
        );
      });
    }
    

    Response - Service unavailable.
    This isn't just slow — it crashes outright. Default memory (256 MiB) is exceeded at 2000+ concurrent queries (works fine at 1000, ~2.5s); the container gets OOM-killed mid-request:

    {
      "textPayload": "The request failed because either the HTTP response was malformed or connection to the instance had an error. Additional troubleshooting documentation can be found at: https://cloud.google.com/run/docs/troubleshooting#malformed-response-or-connection-error\nWhile handling this request, the container instance was found to be using too much memory and was terminated. This is likely to cause a new container instance to be used for the next request to this revision. If you see this message frequently, you may have a memory leak in your code or may need more memory. Consider creating a new revision with more memory.",
      "insertId": "6a4e144a0007936c1f83dfcd",
      "httpRequest": {
        "requestMethod": "GET",
        "requestUrl": "https://issue297-repro-c37rgip73q-uc.a.run.app/issue297repro?n=3000",
        "requestSize": "289",
        "status": 503,
        "responseSize": "67",
        "userAgent": "curl/8.7.1",
        "remoteIp": "102.216.202.19",
        "serverIp": "34.143.79.2",
        "latency": "5.792330713s",
        "protocol": "HTTP/1.1"
      },
      "resource": {
        "type": "cloud_run_revision",
        "labels": {
          "configuration_name": "issue297-repro",
          "project_id": "dart-firebase-admin",
          "revision_name": "issue297-repro-00001-tn5",
          "location": "us-central1",
          "service_name": "issue297-repro"
        }
      },
      "timestamp": "2026-07-08T09:11:32.694130Z",
      "severity": "ERROR",
      "labels": {
        "instanceId": "002f8ffd47aa5d4f0104c7c8bd0277e3171d1f5fea6c5cf3a091a960a532b25bf8d5badb470aeb2b7e7c51a663c52fce44c76d591eb4bfadec021c6792235c179cd11f3c37b86461d0a94025b94fff",
        "goog-managed-by": "firebase-functions"
      },
      "logName": "projects/dart-firebase-admin/logs/run.googleapis.com%2Frequests",
      "trace": "projects/dart-firebase-admin/traces/ba025a4c1a5b84d061c1ab4aca24bb96",
      "receiveTimestamp": "2026-07-08T09:11:38.504277509Z",
      "spanId": "1b4ce013f30d31c7",
      "traceSampled": true
    }

    The follow-up system log names the actual number:

    {
      "textPayload": "Memory limit of 256 MiB exceeded with 266 MiB used. Consider increasing the memory limit, see https://cloud.google.com/run/docs/configuring/memory-limits",
      "insertId": "6a4e144a000757e9d7ea0eaf",
      "resource": {
        "type": "cloud_run_revision",
        "labels": {
          "revision_name": "issue297-repro-00001-tn5",
          "project_id": "dart-firebase-admin",
          "configuration_name": "issue297-repro",
          "service_name": "issue297-repro",
          "location": "us-central1"
        }
      },
      "timestamp": "2026-07-08T09:11:38.481257Z",
      "severity": "ERROR",
      "labels": {
        "goog-managed-by": "firebase-functions",
        "instanceId": "002f8ffd47aa5d4f0104c7c8bd0277e3171d1f5fea6c5cf3a091a960a532b25bf8d5badb470aeb2b7e7c51a663c52fce44c76d591eb4bfadec021c6792235c179cd11f3c37b86461d0a94025b94fff",
        "run.googleapis.com/base_image_versions": "us-docker.pkg.dev/serverless-runtimes/google-24/runtimes/osonly24:osonly24_20260607__RC00"
      },
      "logName": "projects/dart-firebase-admin/logs/run.googleapis.com%2Fvarlog%2Fsystem",
      "receiveTimestamp": "2026-07-08T09:11:38.487919294Z",
      "errorGroups": [
        {
          "id": "CKnV1_TJwMrDQw"
        }
      ]
    }
  6. OutdatedGuy commented on Jul 8, 2026

    @OutdatedGuy
    ContributorAuthor

    @brianquinlan he's piping through cloud functions, so maybe a lack of resources on the platform (Cloud Run)?

    @demolaf I forgot to mention, but I'm running the cloud function through local firebase emulator, and not through deployed Cloud Run. I haven't tried it with deployed functions.

    I'm running only functions through firebase emulator and these functions are calling production APIs for Firestore.

  7. brianquinlan commented on Jul 8, 2026

    @brianquinlan
    Contributor

    I'm running only functions through firebase emulator and these functions are calling production APIs for Firestore.

    @OutdatedGuy what platform are you on?

  8. demolaf commented on Jul 8, 2026

    @demolaf
    Member

    @brianquinlan maybe the 57s was specific to network/machine conditions? I'm getting roughly the same values.

    Using functions emulator only:

    concurrency result
    100 1.62s
    500 1.99s
    1000 3.38s
    2000 3.09s
    3000 3.54s, 0 errors
  9. OutdatedGuy commented on Jul 8, 2026

    @OutdatedGuy
    ContributorAuthor

    @OutdatedGuy what platform are you on?

    I am on MacBook M1 Pro

  10. brianquinlan commented on Jul 8, 2026

    @brianquinlan
    Contributor

    @OutdatedGuy Was the firestore database that were testing against populated in any way?

    Because I was also testing on a Mac and my numbers were very different than yours. Oh, what Dart version are you using?

  11. OutdatedGuy commented on Jul 8, 2026

    @OutdatedGuy
    ContributorAuthor

    @OutdatedGuy Was the firestore database that were testing against populated in any way?

    Yes, the collection of the production database has 5k+ documents, each document having around 15 fields with string values.

    Oh, what Dart version are you using?

    I'm on Dart 3.12.2

  12. brianquinlan commented on Jul 13, 2026

    @brianquinlan
    Contributor

    Hi @OutdatedGuy, can you provide a self-contained example that we can run to reproduce this issue?

  13. OutdatedGuy commented on Jul 13, 2026

    @OutdatedGuy
    ContributorAuthor

    @brianquinlan Here you go, https://github.com/OutdatedGuy/firebase-admin-dart-issues-297

    I have included the steps to reproduce, my testing results and logs in the readme

  14. brianquinlan commented on Jul 16, 2026

    @brianquinlan
    Contributor

    @demolaf are you going to take a look at the repo?

  15. demolaf commented on Jul 17, 2026

    @demolaf
    Member

    Reproduced the same issue from the repro sample provided, see the next comment which tested against an unpublished google_cloud_firestore package.

    Run Dart (hello-world) Node (helloWorld)
    1 11347.582792 ms 32156.1335 ms
    2 10503.417042 ms 9896.514167 ms
    3 11295.328792 ms 10727.0925 ms

    Firebase Emulator Logs

    Time Level Source Message
    10:20:00 I functions Watching <PROJECT_DIR>/dartfunctions for Cloud Functions...
    10:20:02 I functions Loaded functions definitions from source: hello-world.
    10:20:02 I functions http function initialized (http://127.0.0.1:5001/<PROJECT_ID>/us-central1/hello-world).
    10:20:02 I functions Starting build_runner watch for Dart functions...
    10:20:03 I functions build_runner initial build completed
    10:20:03 I functions Watching <PROJECT_DIR>/tsfunctions for Cloud Functions...
    10:20:08 I functions Loaded functions definitions from source: helloWorld.
    10:20:08 I functions http function initialized (http://127.0.0.1:5001/<PROJECT_ID>/us-central1/helloWorld).

    Dart (hello-world) executions

    Run 1 — 10:21:23 to 10:21:34 (11347.582792ms)

    {
      "severity": "ERROR",
      "message": "ClientException: Connection closed before full header was received, uri=https://firestore.googleapis.com/v1/projects/<PROJECT_ID>/databases/(default)/documents:runQuery"
    }

    Stack: package:http/src/io_client.dart 229:7 IOClient.send → package:googleapis_auth/src/auth_http_utils.dart 43:22 AuthenticatedClient.send → package:google_cloud_rpc/service_client.dart 149:22 ServiceClient._makeStreamingRequest

    Run 2 — 10:21:49 to 10:21:59 (10503.417042ms)

    {
      "severity": "ERROR",
      "message": "ClientException: Connection closed before full header was received, uri=https://firestore.googleapis.com/v1/projects/<PROJECT_ID>/databases/(default)/documents:runQuery"
    }

    Stack: same as Run 1.

    Run 3 — 10:22:02 to 10:22:13 (11295.328792ms)

    {
      "severity": "ERROR",
      "message": "HandshakeException: Connection terminated during handshake"
    }

    Node (helloWorld) executions

    Run 1 — 10:22:20 to 10:22:52 (32156.1335ms)

    ⚠️ Google API requested!

    • URL: https://oauth2.googleapis.com/token
    • Be careful, this may be a production service.

    Run 2 — 10:22:58 to 10:23:08 (9896.514167ms)

    No warnings or errors.

    Run 3 — 10:23:11 to 10:23:21 (10727.0925ms)

    No warnings or errors.

  16. demolaf commented on Jul 17, 2026

    @demolaf
    Member

    @brianquinlan

    Working from the local google_cloud_firestore using pubspec overrides with the same repro produces a different result. No HandshakeException either.

    I re-ran @OutdatedGuy's repro (firebase-admin-dart-issues-297) exactly as-is, except I pointed dartfunctions/pubspec_overrides.yaml at local (against main) paths for both packages instead of a git ref on firebase_admin_sdk alone:

    dependency_overrides:
      firebase_admin_sdk:
        path: <local checkout>/packages/firebase_admin_sdk
      google_cloud_firestore:
        path: <local checkout>/packages/google_cloud_firestore

    With both packages genuinely pointed at the fixed code, I ran the same 1500-concurrent-query repro 6 times against production Firestore:

    Run Dart Node
    1 6976ms 2086ms
    2 2404ms 1721ms
    3 2254ms 1154ms
    4 4639ms —
    5 2465ms —
    6 3985ms —

    Zero errors across all 6 — no ClientException, no HandshakeException. Previously this same repro reliably failed with ClientException: Connection closed before full header was received around run 1.

    Dart is still consistently 2-3x slower than Node (2.2-7s vs 1.1-2.1s), so the underlying performance gap still exists — but the crash is gone once PR #294 is actually in the dependency graph.

  17. OutdatedGuy commented on Jul 17, 2026

    @OutdatedGuy
    ContributorAuthor

    I tried @demolaf's changes, and crashes stopped. Seems like I was supposed to override google_cloud_firestore and not firebase_admin_sdk 🤦‍♂️

    Dart is still consistently 2-3x slower than Node (2.2-7s vs 1.1-2.1s)

    @demolaf can you check by increasing query count to 3000 from 1500 and then to 5000. Cuz I believe it's exponential rather than linear. I hit Quota limits while testing the 5K once, so can't check that one, but here are my test results for 1500 & 3000 queries after the crash fixed:

    Run Queries Node Dart
    1 1.5k 1511ms 16457ms
    2 1.5k 1488ms 16500ms
    3 1.5k 1362ms 16565ms
    4 3k 1775ms 32105ms
    5 3k 2317ms 31801ms
    6 3k 1596ms 34141ms
    7 5k 2102ms Quota Exceeded

    I have updated my repo with the crash fixed version and new testing results.

  18. demolaf commented on Jul 17, 2026

    @demolaf
    Member
    • 3000 queries
    Run Dart (hello-world) Node (helloWorld)
    1 12117ms 1846ms
    2 2513ms 2003ms
    3 3606ms 1448ms
    4 3154ms 1886ms
    5 3311ms 1844ms
    6 2855ms 1736ms
    • 5000 queries
    Run Dart (hello-world) Node (helloWorld)
    1 20568ms 2302ms
    2 9984ms 3484ms
    3 6004ms 2173ms
    4 4449ms 2195ms
    5 6170ms —
  19. self-assigned this
    on Jul 23, 2026
  20. demolaf commented on Jul 23, 2026

    @demolaf
    Member

    Working on a solution using http/2 with pooled connections (open new connections after max 100 requests/streams), similar to how node's ClientPool works.

    The current problem with http/1 - opens a connection (TCP/TLS Handshake) on every request, so if there are 3000 it would open 3000 connections all at the same time which can cause out of memory (OOM errors) and slow responses.

    http/2 solves this by allowing multiple requests in a single connection (one TCP/TLS Handshake) and with pooled (open a new connection when an active connection is full at 100 requests/streams) connections allows processing concurrent requests simultaneously.

  21. demolaf commented on Jul 23, 2026

    @demolaf
    Member

    @OutdatedGuy can you try testing from this PR #305 and share your benchmark results?

  22. OutdatedGuy commented on Jul 23, 2026

    @OutdatedGuy
    ContributorAuthor

    @demolaf worked like a charm. Awesome work 🙌🏼

    Run Queries Node.js Dart Dart (Fixed)
    1 1.5k 1671 12104 1423
    2 1.5k 1431 16843 1010
    3 3k 1879 32042 1927
    4 5k 2567 Function Timeout (> 60s) 2336
  23. demolaf commented on Jul 24, 2026

    @demolaf
    Member

    @OutdatedGuy awesome, thanks for catching this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions