Home → Documentation → Performance → Benchmarks
Conditions: No application-level caching. PostgreSQL built-in query cache only. Tool: Apidog Date: June 2026
Each test executes a full round-trip against a live PostgreSQL instance:
INSERT ... ON CONFLICT DO UPDATESELECT with nested LEFT JOIN / JOIN chains that resolves the entire object graph in one database callThe Product model in these tests spans 4 physical tables (Banking.CustomerBankingRelationship, Lending.Contract, Account.Account, Lending.Transaction). A single GraphQL query touching one customer with one product generates:
Before the first request is served, GraphWarmup.Init executes a full warmup pipeline:
IMappingSet implementations and registers them against both enum axes (model type + entity type)MappingWarmup.WarmupMap walks every FieldMap and stores the resolved PropertyInfo objects in NodeMap.ModelProperties and NodeMap.EntityProperties, eliminating per-request Type.GetProperty callsBulkMapper.Compile builds Expression-based getter and setter delegates compiled to IL via Expression.Lambda.Compile(), stored in ConcurrentDictionary keyed by TypeFullName.PropertyNameNodeTreeIterator.GenerateTree pre-builds the full traversal tree for every root mapping, so query planning at request time walks a pre-computed structure rather than reflecting over typesBy the time the first GraphQL request arrives, the mapping layer has no reflection work left to do. Every property read and write goes through a cached compiled delegate.
At request time the three phases are:
| Phase | Mechanism | Reflection cost |
|---|---|---|
| SQL generation | Pre-built NodeTree traversal + string assembly | None |
| PostgreSQL execution | Single batched statement via Dapper | None |
| Entity-to-model mapping | Mapper.MapByAlias → compiled getter/setter delegates |
None |
The Mapper uses three ConcurrentDictionary caches — _propCache, _getterCache, _setterCache — populated during warmup. MapByAlias resolves the NodeMap by alias, iterates FieldMaps, and copies values using the pre-compiled delegates with no runtime reflection.
eq filter)Scenario: One customer with one product per dataset. Fully random data per iteration.
| Metric | Value |
|---|---|
| Datasets | 5 |
| Iterations Executed | 5 |
| Iterations Failed | 0 |
| Assertions Executed | 10 |
| Assertions Failed | 0 |
| Pass Rate | 100% |
| Total Duration | 239 ms |
| Max Response Time | 67 ms |
| Avg Response Time | 13 ms |
Per-dataset response times:
| Dataset | Response Time |
|---|---|
| Dataset-1 | 15 ms |
| Dataset-2 | 13 ms |
| Dataset-3 | 13 ms |
| Dataset-4 | 12 ms |
| Dataset-5 | 14 ms |
mutation a {
wrapper(
wrapper: {
model: INNER_CUSTOMER
customerCustomerEdge: [
{
innerCustomer: {
customerKey: ""
customerType: PERSON
firstNaming: ""
fullNaming: ""
lastNaming: ""
product: [
{
accountKey: ""
accountName: "123AN"
accountNumber: "321AN"
amount: 100
balance: 1200
contractKey: ""
transactionKey: ""
productType: CREDIT_CARD
customerKey: ""
customerBankingRelationshipKey: ""
}
]
}
}
]
cacheKey: "2c0c7698-465f-4fbb-a8c1-9614f7ec6c05"
}
where: {
customerCustomerEdge: {
some: {
innerCustomer: {
customerKey: {
eq: ""
}
}
}
}
}
) {
edges {
node {
customerCustomerEdge {
innerCustomer {
customerKey
customerType
firstNaming
fullNaming
lastNaming
product {
contractKey
accountName
accountNumber
amount
balance
}
}
}
}
}
}
}
This single GraphQL mutation compiles into 10 upsert statements followed by 1 SELECT.
INSERT INTO "Lending"."Transaction" ("Amount", "Balance", "TransactionKey")
VALUES ('100', '1200', '9875df6c-42c3-4630-b944-c221de665a66')
ON CONFLICT ("TransactionKey") DO UPDATE SET
"Amount" = EXCLUDED."Amount",
"Balance" = EXCLUDED."Balance",
"TransactionKey" = EXCLUDED."TransactionKey";
INSERT INTO "Account"."Account" ("AccountKey", "AccountName", "AccountNumber")
VALUES ('b14ed96f-466e-4176-a7d2-66f9088ac384', '123AN', '321AN')
ON CONFLICT ("AccountKey") DO UPDATE SET
"AccountKey" = EXCLUDED."AccountKey",
"AccountName" = EXCLUDED."AccountName",
"AccountNumber" = EXCLUDED."AccountNumber";
INSERT INTO "Lending"."Contract" ("Amount", "ContractKey", "ContractType")
VALUES ('100', '76dea764-8c7f-474d-98bd-6833d0f92fb5', '0')
ON CONFLICT ("ContractKey") DO UPDATE SET
"Amount" = EXCLUDED."Amount",
"ContractKey" = EXCLUDED."ContractKey",
"ContractType" = EXCLUDED."ContractType";
INSERT INTO "Banking"."CustomerBankingRelationship" ("CustomerBankingRelationshipKey")
VALUES ('d94fbf63-fff5-4523-ba1c-9f12dae2c600')
ON CONFLICT ("CustomerBankingRelationshipKey") DO UPDATE SET
"CustomerBankingRelationshipKey" = EXCLUDED."CustomerBankingRelationshipKey";
INSERT INTO "Banking"."Customer" ("CustomerKey", "CustomerType", "FirstName", "FullName", "LastName")
VALUES ('23e8761f-6373-434c-90fb-5359ffa93ff7', '0', 'Cristopher', 'Molly Greenholt', 'Hane')
ON CONFLICT ("CustomerKey") DO UPDATE SET
"CustomerKey" = EXCLUDED."CustomerKey",
"CustomerType" = EXCLUDED."CustomerType",
"FirstName" = EXCLUDED."FirstName",
"FullName" = EXCLUDED."FullName",
"LastName" = EXCLUDED."LastName";
-- Resolve Customer → CustomerBankingRelationship
INSERT INTO "Banking"."CustomerBankingRelationship"
("CustomerId", "CustomerKey", "CustomerBankingRelationshipKey")
(SELECT c."Id", c."CustomerKey", 'd94fbf63-fff5-4523-ba1c-9f12dae2c600'
FROM "Banking"."Customer" c
WHERE "CustomerKey" = '23e8761f-6373-434c-90fb-5359ffa93ff7')
ON CONFLICT ("CustomerBankingRelationshipKey") DO UPDATE SET
"CustomerId" = EXCLUDED."CustomerId",
"CustomerKey" = EXCLUDED."CustomerKey",
"CustomerBankingRelationshipKey" = EXCLUDED."CustomerBankingRelationshipKey";
-- Resolve CustomerBankingRelationship → Contract
INSERT INTO "Lending"."Contract"
("CustomerBankingRelationshipId", "CustomerBankingRelationshipKey", "ContractKey")
(SELECT cbr."Id", cbr."CustomerBankingRelationshipKey", '76dea764-8c7f-474d-98bd-6833d0f92fb5'
FROM "Banking"."CustomerBankingRelationship" cbr
WHERE "CustomerBankingRelationshipKey" = 'd94fbf63-fff5-4523-ba1c-9f12dae2c600')
ON CONFLICT ("ContractKey") DO UPDATE SET
"CustomerBankingRelationshipId" = EXCLUDED."CustomerBankingRelationshipId",
"CustomerBankingRelationshipKey" = EXCLUDED."CustomerBankingRelationshipKey",
"ContractKey" = EXCLUDED."ContractKey";
-- Resolve Account → Contract
INSERT INTO "Lending"."Contract" ("AccountId", "AccountKey", "ContractKey")
(SELECT a."Id", a."AccountKey", '76dea764-8c7f-474d-98bd-6833d0f92fb5'
FROM "Account"."Account" a
WHERE "AccountKey" = 'b14ed96f-466e-4176-a7d2-66f9088ac384')
ON CONFLICT ("ContractKey") DO UPDATE SET
"AccountId" = EXCLUDED."AccountId",
"AccountKey" = EXCLUDED."AccountKey",
"ContractKey" = EXCLUDED."ContractKey";
-- Resolve Contract → Transaction
INSERT INTO "Lending"."Transaction" ("ContractId", "ContractKey", "TransactionKey")
(SELECT c."Id", c."ContractKey", '9875df6c-42c3-4630-b944-c221de665a66'
FROM "Lending"."Contract" c
WHERE "ContractKey" = '76dea764-8c7f-474d-98bd-6833d0f92fb5')
ON CONFLICT ("TransactionKey") DO UPDATE SET
"ContractId" = EXCLUDED."ContractId",
"ContractKey" = EXCLUDED."ContractKey",
"TransactionKey" = EXCLUDED."TransactionKey";
-- Resolve Account → Transaction
INSERT INTO "Lending"."Transaction" ("AccountId", "AccountKey", "TransactionKey")
(SELECT a."Id", a."AccountKey", '9875df6c-42c3-4630-b944-c221de665a66'
FROM "Account"."Account" a
WHERE "AccountKey" = 'b14ed96f-466e-4176-a7d2-66f9088ac384')
ON CONFLICT ("TransactionKey") DO UPDATE SET
"AccountId" = EXCLUDED."AccountId",
"AccountKey" = EXCLUDED."AccountKey",
"TransactionKey" = EXCLUDED."TransactionKey";
SELECT
Customer."CustomerKey",
Customer."CustomerType",
Customer."FirstName",
Customer."FullName",
Customer."LastName",
CBR."Id" AS "Id____",
CBR."CustomerId" AS "CustomerId____",
Contract."Id" AS "Id_____",
Contract."CustomerBankingRelationshipId" AS "CustomerBankingRelationshipId_____",
Contract."ContractKey" AS "ContractKey_____",
Contract."Amount" AS "Amount_____",
Contract."AccountId" AS "AccountId_____",
Account."Id" AS "Id______",
Account."AccountName" AS "AccountName______",
Account."AccountNumber" AS "AccountNumber______",
Transaction."Id" AS "Id_______",
Transaction."ContractId" AS "ContractId_______",
Transaction."AccountId" AS "AccountId_______",
Transaction."Balance" AS "Balance_______"
FROM "Banking"."Customer" Customer
LEFT JOIN (
SELECT CBR."Id", CBR."CustomerId",
Contract."Id" AS "Id_____",
Contract."CustomerBankingRelationshipId" AS "CustomerBankingRelationshipId_____",
Contract."ContractKey" AS "ContractKey_____",
Contract."Amount" AS "Amount_____",
Contract."AccountId" AS "AccountId_____",
Account."Id" AS "Id______",
Account."AccountName" AS "AccountName______",
Account."AccountNumber" AS "AccountNumber______",
Transaction."Id" AS "Id_______",
Transaction."ContractId" AS "ContractId_______",
Transaction."AccountId" AS "AccountId_______",
Transaction."Balance" AS "Balance_______"
FROM "Banking"."CustomerBankingRelationship" CBR
JOIN (
SELECT Contract."Id", Contract."CustomerBankingRelationshipId",
Contract."ContractKey", Contract."Amount", Contract."AccountId",
Account."Id" AS "Id______",
Account."AccountName" AS "AccountName______",
Account."AccountNumber" AS "AccountNumber______",
Transaction."Id" AS "Id_______",
Transaction."ContractId" AS "ContractId_______",
Transaction."AccountId" AS "AccountId_______",
Transaction."Balance" AS "Balance_______"
FROM "Lending"."Contract" Contract
JOIN (
SELECT Account."Id", Account."AccountName", Account."AccountNumber",
Transaction."Id" AS "Id_______",
Transaction."ContractId" AS "ContractId_______",
Transaction."AccountId" AS "AccountId_______",
Transaction."Balance" AS "Balance_______"
FROM "Account"."Account" Account
JOIN (
SELECT Transaction."Id", Transaction."ContractId",
Transaction."AccountId", Transaction."Balance"
FROM "Lending"."Transaction" Transaction
) Transaction ON Account."Id" = Transaction."AccountId_______"
) Account ON Contract."AccountId" = Account."Id______"
) Contract ON CBR."Id" = Contract."CustomerBankingRelationshipId_____"
) CBR ON Customer."Id" = CBR."CustomerId"
WHERE (Customer."CustomerKey" = '23e8761f-6373-434c-90fb-5359ffa93ff7');
Join depth: 5 tables · 4 JOIN levels · 3 schemas (Banking, Lending, Account) · 1 round trip
After the SELECT returns, QueryHandler.MappingConfiguration groups rows by root entity key, deduplicates, then calls Mapper.MapByAlias for each alias. Because BulkMapper.Compile ran at startup, every property read and write goes through a pre-compiled Expression delegate — no Type.GetProperty or PropertyInfo.GetValue calls occur at this stage.
in filter, batch)Scenario: Three customers each with one product per dataset. All three upserted and queried in a single GraphQL operation.
| Metric | Value |
|---|---|
| Datasets | 5 |
| Iterations Executed | 5 |
| Iterations Failed | 0 |
| Assertions Executed | 30 |
| Assertions Failed | 0 |
| Pass Rate | 100% |
| Total Duration | 239 ms |
| Max Response Time | 78 ms |
| Avg Response Time | 16 ms |
Per-dataset response times:
| Dataset | Response Time |
|---|---|
| Dataset-1 | 14 ms |
| Dataset-2 | 20 ms |
| Dataset-3 | 17 ms |
| Dataset-4 | 14 ms |
| Dataset-5 | 13 ms |
mutation a {
wrapper(
wrapper: {
model: INNER_CUSTOMER
cacheKey: "2c0c7698-465f-4fbb-a8c1-9614f7ec6c05"
customerCustomerEdge: [
{
innerCustomer: {
customerKey: ""
customerType: PERSON
firstNaming: ""
fullNaming: ""
lastNaming: ""
product: [
{
accountKey: ""
accountName: "123AN"
accountNumber: "321AN"
amount: 100
balance: 1200
contractKey: ""
transactionKey: ""
productType: CREDIT_CARD
customerKey: ""
customerBankingRelationshipKey: ""
}
]
}
},
{
innerCustomer: {
customerKey: ""
customerType: PERSON
firstNaming: ""
fullNaming: ""
lastNaming: ""
product: [
{
accountKey: ""
accountName: "123AN"
accountNumber: "321AN"
amount: 100
balance: 1200
contractKey: ""
transactionKey: ""
productType: CREDIT_CARD
customerKey: ""
customerBankingRelationshipKey: ""
}
]
}
},
{
innerCustomer: {
customerKey: ""
customerType: PERSON
firstNaming: ""
fullNaming: ""
lastNaming: ""
product: [
{
accountKey: ""
accountName: "123AN"
accountNumber: "321AN"
amount: 100
balance: 1200
contractKey: ""
transactionKey: ""
productType: CREDIT_CARD
customerKey: ""
customerBankingRelationshipKey: ""
}
]
}
}
]
}
where: {
customerCustomerEdge: {
some: {
innerCustomer: {
customerKey: {
in: ["", "", ""]
}
}
}
}
}
) {
edges {
node {
customerCustomerEdge {
innerCustomer {
customerKey
customerType
firstNaming
fullNaming
lastNaming
product {
contractKey
accountName
accountNumber
amount
balance
}
}
}
}
}
}
}
The three-customer mutation scales the same execution pattern: 30 upsert statements (10 per customer) followed by the same 1 SELECT structure with a WHERE ... IN (...) clause. The JOIN shape is identical to Test 1 — only the filter changes.
WHERE (Customer."CustomerKey" IN (
'',
'',
''
))
3× the entities, 3× the upserts, same single SELECT round trip. The mapping layer processes 3× the rows using the same pre-compiled delegates with no additional warmup cost.
Product model spans 4 physical tables across 3 PostgreSQL schemas. In a resolver-chain GraphQL implementation this relationship alone would trigger N+1 queries at every nesting level. Foundgine compiles the entire graph into 1 SELECT regardless of entity count or depth.Expression delegates (populated by BulkMapper.Compile at startup), eliminating reflection overhead from the hot path entirely.Banking, Lending, AccountWrapper → CustomerCustomerEdge → InnerCustomer → Product (Contract + Account + Transaction)| ← Previous: Native AOT | Next: Samples → |