It’s Not Your Language Slowing You Down — It’s Your Architecture

Recently I made the decision to rewrite a portion of my Lipsey’s distributor integration in good ole C using libcurl. I didn’t need to do this, but I was under the “it must be faster because it’s C” mindset.

So I went ahead and and flexed my C muscles, something that I haven’t done in a LONG time, and put together a small little implementation that calls that Catalog data endpoint using a POST request:

#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

// ----------------------
// Timing helpers (because "it feels slow" is not a metric)
// ----------------------
static double now_ms(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1000000.0;
}

// ----------------------
// Dynamic response buffer (cheap, simple, works)
// ----------------------
typedef struct {
    char *data;
    size_t size;
} Memory;

// Curl streams data into here. We just keep realloc’ing like animals.
// Totally fine for API responses.
static size_t write_cb(void *contents, size_t size, size_t nmemb, void *userp) {
    size_t realsize = size * nmemb;
    Memory *mem = (Memory *)userp;

    char *ptr = realloc(mem->data, mem->size + realsize + 1);
    if (!ptr) return 0; // malloc said nope

    mem->data = ptr;
    memcpy(mem->data + mem->size, contents, realsize);
    mem->size += realsize;
    mem->data[mem->size] = '\0';

    return realsize;
}

// ----------------------
// Extremely lazy JSON helpers (string searching > writing parser today)
// ----------------------

static int json_contains(const char *json, const char *needle) {
    return (json && needle && strstr(json, needle) != NULL);
}

// We only care if econtact.success is true or 1. Not building a schema validator today.
static int json_econtact_success_is_true_or_one(const char *json) {
    const char *p = strstr(json, "\"econtact\"");
    if (!p) return 0;

    // Only scan a window after econtact so we don't match random success fields later
    const size_t WINDOW = 800;
    size_t len = strlen(p);
    if (len > WINDOW) len = WINDOW;

    char *tmp = (char *)malloc(len + 1);
    if (!tmp) return 0;

    memcpy(tmp, p, len);
    tmp[len] = '\0';

    int ok = 0;
    if (strstr(tmp, "\"success\":true") ||
        strstr(tmp, "\"success\": true") ||
        strstr(tmp, "\"success\":1") ||
        strstr(tmp, "\"success\": 1")) {
        ok = 1;
    }

    free(tmp);
    return ok;
}

// Pulls token out of `"token":"value"`
// Yes this is brittle. No I do not care for this use case.
static int extract_token(const char *json, char *out_token, size_t max_len) {
    if (!json || !out_token || max_len == 0) return -1;

    const char *t = strstr(json, "\"token\"");
    if (!t) return -1;

    t = strchr(t, ':');
    if (!t) return -1;
    t++;

    while (*t == ' ' || *t == '\t') t++;

    if (*t != '"') return -1;
    t++;

    const char *end = strchr(t, '"');
    if (!end) return -1;

    size_t n = (size_t)(end - t);
    if (n == 0 || n >= max_len) return -1;

    memcpy(out_token, t, n);
    out_token[n] = '\0';
    return 0;
}

// Try to dump API error array if present. Otherwise just dump tail and move on.
static void print_api_errors_best_effort(const char *json) {
    const char *p = strstr(json, "\"errors\"");
    if (!p) {
        fprintf(stderr, "No \"errors\" array found.\n");
        return;
    }

    const char *lb = strchr(p, '[');
    const char *rb = lb ? strchr(lb, ']') : NULL;

    if (lb && rb && rb >= lb) {
        fprintf(stderr, "API errors: ");
        fwrite(lb, 1, (size_t)(rb - lb + 1), stderr);
        fputc('\n', stderr);
    } else {
        fprintf(stderr, "API errors (raw tail): %s\n", p);
    }
}

// ----------------------
// Curl helpers
// ----------------------

// Because env vars are nice when you don’t want creds in source
static int env_truthy(const char *name, int default_val) {
    const char *v = getenv(name);
    if (!v || !*v) return default_val;

    if (strcmp(v, "1") == 0 || strcasecmp(v, "true") == 0 || strcasecmp(v, "yes") == 0)
        return 1;

    if (strcmp(v, "0") == 0 || strcasecmp(v, "false") == 0 || strcasecmp(v, "no") == 0)
        return 0;

    return default_val;
}

// Applies shared curl config so we don't copy/paste 40 lines everywhere
static void apply_common_opts(CURL *curl, int insecure, Memory *out) {
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 10L);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "bickhamfirearms-lipseys/0.1");
    curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_1_1);

    // Equivalent to PHP CURLOPT_ENCODING ""
    curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");

    // Match vendor PHP behavior exactly (yes this disables TLS validation)
    if (insecure) {
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
    } else {
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
    }

    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, out);
}

// ----------------------
// MAIN PROGRAM
// ----------------------

int main(void) {

    const double t_program_start = now_ms();

    // You can swap these to getenv if you want later
    const char *email = "[email protected]";
    const char *password = "Lacro$$e1";

    if (!email || !password) {
        fprintf(stderr,
            "Missing credentials.\n"
            "Set:\n"
            "export LIPSEYS_EMAIL=...\n"
            "export LIPSEYS_PASSWORD=...\n");
        return 1;
    }

    // Default insecure to match vendor PHP exactly
    int insecure = env_truthy("LIPSEYS_INSECURE", 1);

    curl_global_init(CURL_GLOBAL_DEFAULT);

    CURL *curl = curl_easy_init();
    if (!curl) {
        fprintf(stderr, "curl init failed\n");
        curl_global_cleanup();
        return 1;
    }

    const char *BASE = "https://api.lipseys.com/api/";
    const char *LOGIN_PATH = "integration/authentication/login";
    const char *CATALOG_PATH = "integration/items/CatalogFeed";

    // ---------------- LOGIN ----------------
    const double t_login_start = now_ms();

    char login_url[512];
    snprintf(login_url, sizeof(login_url), "%s%s", BASE, LOGIN_PATH);

    char json_body[1024];
    snprintf(json_body, sizeof(json_body),
        "{\"Email\":\"%s\",\"Password\":\"%s\"}",
        email, password);

    Memory login_resp = {0};

    curl_easy_reset(curl);
    apply_common_opts(curl, insecure, &login_resp);

    struct curl_slist *login_headers = NULL;
    login_headers = curl_slist_append(login_headers, "Content-Type: application/json");
    login_headers = curl_slist_append(login_headers, "Accept: application/json");
    login_headers = curl_slist_append(login_headers, "Token: "); // vendor sends even if blank

    curl_easy_setopt(curl, CURLOPT_URL, login_url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, login_headers);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body);

    printf("Logging in...\n");

    CURLcode rc = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    const double t_login_end = now_ms();

    if (rc != CURLE_OK) {
        fprintf(stderr, "Login failed: %s\n", curl_easy_strerror(rc));
        fprintf(stderr, "[PROFILE] login %.2f ms\n", t_login_end - t_login_start);
        goto cleanup;
    }

    printf("[HTTP] Login status: %ld\n", http_code);
    printf("[PROFILE] Login took %.2f ms\n", t_login_end - t_login_start);

    char token[512] = {0};

    int token_ok = extract_token(login_resp.data, token, sizeof(token)) == 0;
    int econtact_ok = json_econtact_success_is_true_or_one(login_resp.data);

    if (!token_ok || !econtact_ok) {
        fprintf(stderr, "Login rejected\n");
        print_api_errors_best_effort(login_resp.data);
        goto cleanup;
    }

    printf("Token extracted OK (len=%zu)\n", strlen(token));

    // ---------------- CATALOG ----------------
    const double t_catalog_start = now_ms();

    char catalog_url[512];
    snprintf(catalog_url, sizeof(catalog_url), "%s%s", BASE, CATALOG_PATH);

    Memory catalog_resp = {0};

    curl_easy_reset(curl);
    apply_common_opts(curl, insecure, &catalog_resp);

    char token_header[1024];
    snprintf(token_header, sizeof(token_header), "Token: %s", token);

    struct curl_slist *catalog_headers = NULL;
    catalog_headers = curl_slist_append(catalog_headers, token_header);
    catalog_headers = curl_slist_append(catalog_headers, "cache-control: no-cache");
    catalog_headers = curl_slist_append(catalog_headers, "Accept: application/json");

    curl_easy_setopt(curl, CURLOPT_URL, catalog_url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, catalog_headers);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET");
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");

    printf("\nFetching catalog...\n");

    rc = curl_easy_perform(curl);

    long catalog_http = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &catalog_http);

    const double t_catalog_end = now_ms();

    if (rc == CURLE_OK) {
        printf("[HTTP] Catalog status: %ld\n", catalog_http);
        printf("[PROFILE] Catalog took %.2f ms\n", t_catalog_end - t_catalog_start);

        printf("\nCatalog preview (first 2000 chars):\n");
        if (catalog_resp.data) {
            size_t n = catalog_resp.size > 2000 ? 2000 : catalog_resp.size;
            fwrite(catalog_resp.data, 1, n, stdout);
            printf("\n");
        }
    }

    // ---------------- TOTAL ----------------
    const double t_program_end = now_ms();
    printf("\n[PROFILE] TOTAL PROGRAM TIME: %.2f ms (%.2f sec)\n",
        t_program_end - t_program_start,
        (t_program_end - t_program_start) / 1000.0);

cleanup:
    curl_slist_free_all(login_headers);
    curl_easy_cleanup(curl);
    curl_global_cleanup();
    free(login_resp.data);
    return 0;
}

So, all of this code… what did it result in? Here ya go:

Catalog response preview (first 2000 chars):
{"success":true,"authorized":true,"errors":[],"data":[{"itemNo":"RU1022RB","description1":"10/22 CARBINE 22LR BL/WD 10+1#","description2":"1103","upc":"736676011032","manufacturerModelNo":"1103","msrp":349.0,"model":"10/22 Carbine","caliberGauge":"22 LR","manufacturer":"Ruger","type":"Rifle","action":"Semi-Auto","barrelLength":"18.5\"","capacity":"10 + 1","finish":"Satin Black","overallLength":"37\"","receiver":null,"safety":null,"sights":"Gold Bead Front/Folding Rear","stockFrameGrips":"Wood Stock Hardwood","magazine":"1 10 rd.","weight":"5 lbs.","imageName":"1103534d.jpg","chamber":null,"drilledAndTapped":true,"rateOfTwist":"1-in-16","itemType":"Firearm","additionalFeature1":"Includes Scope Base","additionalFeature2":"Barrel Band Stock","additionalFeature3":null,"shippingWeight":6.25,"boundBookManufacturer":"Ruger","boundBookModel":"10/22","boundBookType":"Rifle","nfaThreadPattern":null,"nfaAttachmentMethod":null,"nfaBaffleType":null,"silencerCanBeDisassembled":false,"silencerConstructionMaterial":null,"nfaDbReduction":null,"silencerOutsideDiameter":null,"nfaForm3Caliber":null,"opticMagnification":null,"maintubeSize":null,"adjustableObjective":false,"objectiveSize":null,"opticAdjustments":null,"illuminatedReticle":false,"reticle":null,"exclusive":false,"quantity":0,"allocated":false,"canDropship":false,"onSale":false,"price":199.0,"currentPrice":199.0,"retailMap":0.0,"fflRequired":true,"sotRequired":false,"exclusiveType":"","scopeCoverIncluded":false,"special":null,"sightsType":"Adjustable Sights","case":null,"choke":null,"dbReduction":null,"family":"10/22 Series","finishType":"Blued","frame":null,"gripType":"Hardwood","handgunSlideMaterial":null,"countryOfOrigin":"US","itemLength":"40.50","itemWidth":"6.50","itemHeight":"14.50","packageLength":"40.0000","packageWidth":"5.9000","packageHeight":"2.9000","itemGroup":"Sporting Semi-Auto Rimfire Rifles"},{"itemNo":"SP0011710305-F","description1":"PH3 6MMCR MOUNTAIN SHADOW 20\"","description2":"0011710305-F","upc":"811

[PROFILE] TOTAL PROGRAM TIME: 7309.60 ms (7.31 sec)

A total run time of 7.3 seconds… the exact same as the PHP implementation that is within our current code. But how can this be?

Well, like most software engineers, I took the arrogant perspective that I TOTALLY could write something faster than what the default PHP implementation could be. In reality, the PHP implementation uses the SAME EXACT curl calls that are all C under the hood. I spent all this time writing this code only to write the exact code that was running this entire time. Funny. At the end of the day, this Catalog refresh pipeline is dominated by the 7 second response time it takes to grab all that sweet, sweet catalog data.


The Reality Most Developers Don’t Want to Hear

If your program:

  • Calls remote APIs
  • Waits on databases
  • Pulls files over FTP / HTTPS
  • Talks to third-party vendors

Then your performance is dominated by:

  • Network latency
  • Remote server processing
  • TLS handshakes
  • Payload transfer time

Not your language.

You can rewrite PHP → C → Rust → Assembly → Hand-written transistor logic.

If the server takes 7 seconds to respond, you’re still waiting 7 seconds. Period.


The Hidden Truth: You’re Probably Already Running C Anyway

Here’s the funny part.

Lipsey’s PHP example uses the PHP cURL extension as I stated above.

The PHP cURL extension is just a wrapper around:

  • libcurl (C)
  • OpenSSL (C)
  • OS networking stack (C / kernel space)

So the real execution path looks like:

PHP Version

PHP Script
 ↓
PHP cURL Extension
 ↓
libcurl (C)
 ↓
OpenSSL (C)
 ↓
Kernel TCP Stack
 ↓
Internet

My C Version

My C Program
 ↓
libcurl (C)
 ↓
OpenSSL (C)
 ↓
Kernel TCP Stack
 ↓
Internet

So once the request leaves your process…

You’re in C anyway.


Where The Time Actually Goes

When you break down HTTP requests, time is usually spent in:

DNS Lookup

Usually tiny.

TCP Connect

Usually small unless cross-region.

TLS Handshake

Moderate but predictable.

Time To First Byte (TTFB)

This is usually the killer.

TTFB includes:

  • Vendor backend processing
  • Cache lookups
  • DB queries
  • Internal microservice calls
  • Queue waits

If TTFB is 5 seconds, you’re done. That’s your bottleneck. No more speedups or optimizations for you past this hard wall. Think of it like having 10 seconds of work, but only 5 seconds of it can be parallelizable. You are now stuck with 5 seconds of sequential runtime that wont be going away.


What Actually Makes Systems Faster

Not rewriting in C.

Usually:

Fewer Requests

Don’t poll APIs that update every 4 hours… every 5 minutes.

Smarter Scheduling

Run ingestion when vendor data actually changes.

Caching

Token caching. Response caching. Etc.

Connection Reuse

Keep TCP + TLS sessions alive.

Streaming Instead of Buffering

Especially for giant feeds. I ended up getting massive memory savings as explained in my last post by streaming to a TSV.


The Dangerous Developer Trap

Developers love optimizing:

  • Language choice
  • Micro-allocations
  • Loop performance
  • Branch prediction
  • SIMD tricks

Meanwhile:

They make 5 redundant API calls per request.


The Architecture Mindset Shift

Good performance thinking sometimes actually boils down to this:

Instead of:

“How do I make this code faster?”

Ask:

“How do I do less work?”

Instead of:

“How do I optimize this loop?”

Ask:

“Why does this loop exist?”

Instead of:

“Should I rewrite this in C?”

Ask:

“Why am I making this network call at all?”


The Real Lesson

My goal was to beat PHP with C. Then it quickly was not my goal when I realized the folly of my ways.

In reality, we answered:

“Is my architecture the bottleneck?”

The answer was yes.

And that’s valuable.

Because now I know where my real speed-ups occur:

  • Caching vendor feeds
  • Reducing unnecessary logins
  • Scheduling ingestion intelligently

Thankfully, I already had the foresight to implement these things, but only after banging my head against the wall when I saw that my VPS CPU time was being eaten 24/7. But you live and you learn I suppose.


Final Thought

Languages matter.

But architecture matters more.

If you’re I/O bound:
Language gains are noise.

If you’re CPU bound:
Language matters.

Most modern backend systems?

Are I/O bound.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *