{"id":160,"date":"2026-03-05T12:53:16","date_gmt":"2026-03-05T12:53:16","guid":{"rendered":"https:\/\/magendoo.ro\/insights\/?p=160"},"modified":"2026-03-05T12:53:16","modified_gmt":"2026-03-05T12:53:16","slug":"magento-not-worker-system-go","status":"publish","type":"post","link":"https:\/\/magendoo.ro\/insights\/magento-not-worker-system-go\/","title":{"rendered":"Magento Is Not a Worker System: Put the Heavy Lifting in Go"},"content":{"rendered":"<p>PHP is great at commerce. It\u2019s terrible at heavy async work. Stop pretending otherwise.<\/p>\n<p>If your ERP exports run on Magento cron, your pricing sync blocks checkout performance, and your shipment tracking retries live inside a plugin \u2014 you didn\u2019t build a commerce platform. You built a monolith that happens to sell things.<\/p>\n<p>I\u2019ve seen this in project after project: teams push processing work into Magento because it\u2019s there, it\u2019s familiar, and nobody wants to introduce another language. The result is always the same \u2014 a system that gets slower, harder to debug, and more fragile with every integration you add.<\/p>\n<p>There\u2019s a better way. And it starts with drawing a line.<\/p>\n<h2 class=\"wp-block-heading\">Why This Keeps Happening<\/h2>\n<p>Magento is a full-stack framework. It has cron, queues (via RabbitMQ or MySQL), observers, plugins, and a CLI. So when a new integration requirement comes in \u2014 sync prices from SAP, push orders to an ERP, track shipments from a carrier API \u2014 the path of least resistance is obvious: write a module, schedule a cron, call it done.<\/p>\n<p>Here\u2019s why teams keep making this choice:<\/p>\n<ul>\n<li><strong>Familiarity.<\/strong> The team knows PHP and Magento\u2019s DI container. Introducing Go means hiring or upskilling.<\/li>\n<li><strong>Agency incentives.<\/strong> Agencies bill Magento hours. A Go microservice is out of scope, harder to estimate, and doesn\u2019t fit the SOW.<\/li>\n<li><strong>Platform defaults.<\/strong> Magento ships with <code>cron_schedule<\/code>, <code>queue_topology.xml<\/code>, and <code>MessageQueueConsumerInterface<\/code>. It looks like it was designed for this. It wasn\u2019t.<\/li>\n<li><strong>Short-term thinking.<\/strong> The first integration works fine in cron. The fifth one doesn\u2019t. But by then, the architecture is set.<\/li>\n<\/ul>\n<p>The problem isn\u2019t that Magento can\u2019t do async work. It can \u2014 technically. The problem is that it does it badly at scale, and every integration you add compounds the cost.<\/p>\n<h2 class=\"wp-block-heading\">What Actually Goes Wrong<\/h2>\n<h3 class=\"wp-block-heading\">Cron Contention<\/h3>\n<p>Magento\u2019s cron runner is a single-threaded scheduler. When you stack 15 custom cron jobs on top of indexers, cache warming, and queue consumers, you get overlap, missed runs, and unpredictable execution order. I\u2019ve debugged production systems where pricing sync didn\u2019t run for 6 hours because a catalog export job was stuck.<\/p>\n<h3 class=\"wp-block-heading\">Memory and Process Limits<\/h3>\n<p>PHP processes are request-scoped by design. Long-running workers fight against PHP\u2019s memory model. You end up writing <code>gc_collect_cycles()<\/code> calls and restarting consumers every N messages to avoid memory leaks. That\u2019s not engineering \u2014 that\u2019s life support.<\/p>\n<h3 class=\"wp-block-heading\">No Native Concurrency<\/h3>\n<p>Need to sync 50,000 prices from an ERP? In Magento, you\u2019re looping sequentially or managing brittle <code>pcntl_fork<\/code> hacks. In Go, you spin up a worker pool with goroutines and channels in 30 lines of code. The difference isn\u2019t marginal \u2014 it\u2019s orders of magnitude.<\/p>\n<h3 class=\"wp-block-heading\">Retry Logic Becomes Business Logic<\/h3>\n<p>When your retry mechanism lives inside Magento, it entangles with the commerce domain. Suddenly your order export module knows about exponential backoff, dead-letter queues, and circuit breakers. That\u2019s integration infrastructure masquerading as commerce code. It doesn\u2019t belong there.<\/p>\n<h2 class=\"wp-block-heading\">Where Go Fits \u2014 And Why<\/h2>\n<p>Go isn\u2019t the answer to everything. But for a specific class of e-commerce problems, it\u2019s the right tool:<\/p>\n<ul>\n<li><strong>High-volume imports\/exports.<\/strong> Catalog feeds, pricing syncs, inventory updates \u2014 anything that processes thousands of records on a schedule. Go handles this with minimal memory, native concurrency, and no framework overhead.<\/li>\n<li><strong>Integration middleware.<\/strong> DTO mapping between Magento\u2019s data model and external systems (SAP, NetSuite, Dynamics). Go\u2019s strong typing and struct marshaling make transformations explicit and testable.<\/li>\n<li><strong>Event-driven workers.<\/strong> Order placed? Shipment created? Go consumers can pick up events from RabbitMQ or Kafka, process them with retries and idempotency, and report back \u2014 without touching Magento\u2019s runtime.<\/li>\n<li><strong>Shipment tracking updates.<\/strong> This is a pattern I\u2019ve extracted into its own service. A warehouse or 3PL drops CSV files with tracking numbers. A Go service watches the directory, parses each file, looks up the order in Magento via REST API, and pushes the tracking data to the shipment \u2014 with a worker pool for concurrency, retry with backoff on API failures, and file lifecycle management (processed vs.\u00a0failed). In Magento, this would be a brittle cron job fighting PHP\u2019s memory model. In Go, it\u2019s a clean daemon that runs indefinitely with a 15MB binary. I open-sourced this as <a href=\"https:\/\/github.com\/florinel-chis\/tracking-updater\">tracking-updater<\/a> \u2014 it\u2019s a good reference for what this boundary looks like in practice.<\/li>\n<li><strong>Search indexing pipelines.<\/strong> Pushing data to Elasticsearch or Algolia at scale. Go can stream, batch, and retry without blocking Magento\u2019s indexer queue.<\/li>\n<\/ul>\n<p>The pattern is consistent: <strong>Magento owns the commerce domain. Go handles the heavy lifting outside it.<\/strong><\/p>\n<h3 class=\"wp-block-heading\">Why Go Specifically?<\/h3>\n<ul>\n<li><strong>Compiled, single binary.<\/strong> Deploy a 15MB binary to a container. No Composer, no PHP extensions, no framework bootstrap.<\/li>\n<li><strong>Goroutines.<\/strong> Lightweight concurrency that doesn\u2019t require threading libraries or process management. The tracking-updater runs 5 concurrent workers processing files in parallel \u2014 each one independently calling Magento\u2019s API. Try doing that reliably in a Magento cron job.<\/li>\n<li><strong>Small memory footprint.<\/strong> A Go worker processing 10,000 messages uses less RAM than a single Magento CLI command bootstrapping the DI container.<\/li>\n<li><strong>Explicit error handling.<\/strong> No silent failures. Every operation returns an error you must handle. This matters when you\u2019re processing financial data between systems.<\/li>\n<\/ul>\n<h2 class=\"wp-block-heading\">The Magento Side of This<\/h2>\n<p>When you extract processing into Go, Magento doesn\u2019t disappear. It just gets focused.<\/p>\n<p>Here\u2019s what Magento does in this pattern:<\/p>\n<ul>\n<li><strong>Emits events.<\/strong> An observer fires on <code>sales_order_place_after<\/code> and publishes a message to RabbitMQ. That\u2019s it. No transformation, no retry logic, no external API calls.<\/li>\n<li><strong>Provides API endpoints.<\/strong> Go services call Magento\u2019s REST API to read catalog data, update stock, or push shipment tracking. Magento serves the data \u2014 it doesn\u2019t orchestrate the workflow.<\/li>\n<li><strong>Exports payloads.<\/strong> A lightweight cron job serializes a batch of updated products into a queue message. The Go service picks it up and handles the rest.<\/li>\n<\/ul>\n<p>The key insight: <strong>Magento becomes a data source and event emitter, not a processing engine.<\/strong> It stays fast, upgradeable, and focused on what it\u2019s good at \u2014 rendering storefronts and managing commerce state.<\/p>\n<h3 class=\"wp-block-heading\">Making the Boundary Clean<\/h3>\n<p>The hardest part of this pattern isn\u2019t the Go code. It\u2019s the interface between Go and Magento. Magento\u2019s REST API is verbose \u2014 search criteria filters, nested extension attributes, inconsistent response structures. If every Go microservice hand-rolls its own HTTP client for Magento, you end up with duplicated auth logic, inconsistent error handling, and DTO structs scattered across repos.<\/p>\n<p>That\u2019s why I built <a href=\"https:\/\/github.com\/florinel-chis\/go-m2rest\">go-m2rest<\/a> \u2014 an open-source Go client library for Magento 2\u2019s REST API. It covers products, orders, categories, attributes, carts, and configurable product management. Authentication (bearer token, admin credentials, customer credentials), automatic retries on 500\/503, and typed Go structs for every Magento entity.<\/p>\n<p>Instead of hand-crafting search criteria URL parameters, you call a typed method. Instead of parsing nested JSON responses into <code>map[string]interface{}<\/code>, you get proper Go structs with explicit fields. The tracking-updater uses it under the hood \u2014 one library handles the Magento boundary, every service on top of it stays focused on its own domain.<\/p>\n<p>This is what \u201cboundary thinking\u201d looks like in practice: <strong>one clean interface between your Go services and Magento, not a dozen ad-hoc integrations.<\/strong><\/p>\n<h2 class=\"wp-block-heading\">Decision Checklist: Extract or Keep?<\/h2>\n<p><strong>Extract to Go when:<\/strong><\/p>\n<ul>\n<li>The job processes more than 1,000 records per run<\/li>\n<li>It requires retries, backoff, or dead-letter handling<\/li>\n<li>It talks to external APIs with rate limits or unreliable uptime<\/li>\n<li>It runs longer than 60 seconds consistently<\/li>\n<li>It needs true concurrency (parallel API calls, batch processing)<\/li>\n<li>The logic has nothing to do with commerce (DTO mapping, file parsing, data reconciliation)<\/li>\n<\/ul>\n<p><strong>Keep in Magento when:<\/strong><\/p>\n<ul>\n<li>It\u2019s tightly coupled to commerce state (cart rules, tax calculation, checkout flow)<\/li>\n<li>It runs in under 10 seconds and processes small batches<\/li>\n<li>It only reads\/writes Magento data with no external system involved<\/li>\n<li>The team has no capacity to operate a second runtime<\/li>\n<li>It\u2019s a one-off migration script, not a recurring process<\/li>\n<\/ul>\n<p>Be honest about the last point. If your team can\u2019t deploy and monitor a Go service today, the answer isn\u2019t \u201cforce Magento to do it\u201d \u2014 it\u2019s \u201cbuild the capability.\u201d But it\u2019s also not \u201crewrite everything in Go next sprint.\u201d<\/p>\n<h2 class=\"wp-block-heading\">The Leadership Question<\/h2>\n<p>If you\u2019re a tech lead or CTO, here\u2019s the real question: <strong>What\u2019s the cost of running integration logic inside your commerce platform?<\/strong><\/p>\n<p>It\u2019s not just performance. It\u2019s:<\/p>\n<ul>\n<li><strong>Upgrade risk.<\/strong> Every custom cron job and queue consumer is another thing that can break during a Magento version upgrade. A Go service that talks to Magento over REST? It doesn\u2019t care what Magento version you\u2019re on \u2014 the API contract is the boundary.<\/li>\n<li><strong>Team bottleneck.<\/strong> Your Magento developers are writing retry logic instead of building commerce features.<\/li>\n<li><strong>Debugging complexity.<\/strong> When an ERP sync fails at 3 AM, your on-call engineer needs to understand both Magento\u2019s DI and SAP\u2019s API. That\u2019s two domains in one codebase. Separate services mean separate logs, separate deployments, separate failure domains.<\/li>\n<li><strong>Scaling cost.<\/strong> You can\u2019t scale integration workers independently of your web nodes. Need more processing power? You\u2019re scaling the entire Magento stack.<\/li>\n<\/ul>\n<p>Extracting heavy processing into Go services isn\u2019t just a technical decision. It\u2019s a business decision about where your team spends its time and how resilient your platform is under pressure.<\/p>\n<h2 class=\"wp-block-heading\">The Line Is the Architecture<\/h2>\n<p>The most important thing you can draw in your system isn\u2019t a class diagram or a sequence diagram. It\u2019s a boundary.<\/p>\n<p>On one side: Magento handles commerce \u2014 catalog, cart, checkout, customer, orders. On the other side: Go handles everything that isn\u2019t commerce \u2014 transformations, integrations, heavy processing, external system communication.<\/p>\n<p>That line is your architecture. Everything else is implementation detail.<\/p>\n<p>Stop asking Magento to be something it was never designed to be. Let it do what it\u2019s good at. And put the heavy lifting where it belongs.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Why integration and processing logic doesn&#8217;t belong inside Magento \u2014 and how Go microservices handle it better.<\/p>\n","protected":false},"author":1,"featured_media":159,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-container-style":"default","site-container-layout":"default","site-sidebar-layout":"default","disable-article-header":"default","disable-site-header":"default","disable-site-footer":"default","disable-content-area-spacing":"default","footnotes":""},"categories":[1],"tags":[],"class_list":["post-160","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-general"],"_links":{"self":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts\/160","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/comments?post=160"}],"version-history":[{"count":1,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts\/160\/revisions"}],"predecessor-version":[{"id":161,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts\/160\/revisions\/161"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/media\/159"}],"wp:attachment":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/media?parent=160"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/categories?post=160"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/tags?post=160"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}