Sources
1577 sources collected
### Lack of Scalability One of the main limitations of SQLite is its lack of scalability compared to other database management systems like MySQL or PostgreSQL. SQLite is designed to be lightweight and self-contained, making it ideal for small to medium-sized projects. However, as the size of the database and the number of concurrent users grow, SQLite may struggle to handle the increased workload efficiently. This can result in performance issues and potential data inconsistencies, which can be a significant drawback for enterprise applications that require high scalability. … ### Weak Security Features SQLite also lacks advanced security features compared to other database management systems. While SQLite does support database encryption and access control mechanisms, it does not offer the same level of security controls as enterprise-grade databases. This can be a concern for developers working on applications that handle sensitive data and require stringent security measures to protect against unauthorized access and data breaches. ### Limited Support for Stored Procedures and Triggers SQLite has limited support for stored procedures and triggers, which are essential features for implementing complex business logic and data manipulation operations in database applications. While SQLite does support triggers for enforcing referential integrity and updating data, it lacks support for stored procedures that allow developers to define custom functions and procedures to be executed on the database server side. This limitation can make it challenging for developers to implement advanced data processing and automation features in their applications, limiting the flexibility and extensibility of SQLite for enterprise use cases. While SQLite is a powerful and lightweight database management system that is well-suited for small to medium-sized projects, it does have limitations when it comes to enterprise-level applications. The lack of scalability, limited concurrency support, weak security features, and limited support for stored procedures and triggers are some of the key limitations of SQLite that developers need to consider when choosing a database management system for their projects. Despite these limitations, SQLite remains a popular choice for developers working on embedded systems, mobile applications, and other lightweight projects where simplicity and efficiency are prioritized over enterprise features. … - Database Locking: SQLite uses a simple locking mechanism to ensure data integrity. However, this can lead to bottlenecks and performance issues when multiple users are trying to access the database simultaneously. - Write Contentions: When multiple users are trying to write to the same database at the same time, SQLite can encounter write contentions. This can result in data corruption or loss if not handled properly. - Deadlocks: In some cases, SQLite may experience deadlocks when two or more transactions are waiting for each other to release locks on resources. This can cause the database to become unresponsive and lead to data inconsistencies. … ## Comments (48) SQLite is a great lightweight database management system for small projects. However, it has its limitations that developers should be aware of.One limitation of SQLite is its lack of scalability. Since it is designed for single-user environments, it can become slow when handling large amounts of data or multiple concurrent users. Even though SQLite is ACID-compliant, it lacks support for SQL features like stored procedures, triggers, and views. This can make it challenging for developers who rely heavily on these features in their applications. One major limitation of SQLite is its lack of user management capabilities. It doesn't have built-in user authentication and authorization features, so developers need to handle security externally. Another limitation of SQLite is its limited datatype support. It doesn't support a wide range of data types like other database management systems, which can be limiting for developers working with complex data structures. … Although SQLite is great for prototyping and small-scale projects, it can struggle when handling concurrent write operations. This can lead to data corruption and performance issues in applications with high write loads. One common mistake developers make with SQLite is using it in scenarios where it doesn't fit well, such as high-traffic websites or enterprise-level applications. Always consider the limitations of SQLite before deciding to use it in your project. … Also, SQLite doesn't support stored procedures or triggers. So if you're all about that SQL magic, you might find yourself hitting a wall. And don't forget about concurrency issues! SQLite doesn't play nicely with multiple write operations at the same time. If you're working with huge datasets, you could experience performance issues. SQLite isn't really optimized for handling massive amounts of data. … You might run into locking issues or slow queries under heavy load. <code> INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'); </code> Also, be aware that SQLite doesn't have great support for data types compared to other databases. You might run into issues with type coercion or unexpected behavior. … Yo, SQLite may be a great lightweight database to work with, but don't forget it has its limitations. For starters, it doesn't support stored procedures, triggers, or views like other databases do. This can make complex data manipulation and retrieval a bit challenging. I was working on a project where I needed to insert a huge amount of data into an SQLite database, and I hit a roadblock due to the lack of support for bulk inserts. Had to resort to inserting each row one by one, which was a pain.
These points are fair. The overarching theme is a pushback against automatically choosing complex, client-server databases like PostgreSQL when SQLite is often more than sufficient, simpler to manage, and faster for the majority of use cases. I agree with that framing. The debate has settled into a well-understood set of tradeoffs: |For "SQLite for everything"|Known limitations| |--|--| |Zero-latency reads as an embedded library|Write concurrency limited to a single writer| |No separate server to set up or maintain|Not designed for distributed or clustered systems| |Reliable, self-contained, battle-tested (most deployed DB in the world)|No built-in user management; relies on filesystem permissions| |Fast enough for most human-driven web workloads|Schema migration can be more complex in large projects| These are the terms of the current discussion. But there is an important, often overlooked dimension missing from this framing. **SQLite struggles with complex queries**. More specifically, SQLite is not well-suited to handle the kind of multi-join queries that arise naturally in any serious production system. This goes beyond the usual talking points about deployment concerns (write concurrency, distribution, and so on). It points to a system-level limitation: the query optimizer itself. That limitation matters even for read-heavy, single-node deployments, which is exactly the use case where SQLite is supposed to shine. … SQLite needed a 60-second timeout per query, and 9 queries failed to complete within that limit. The actual total time for SQLite would be substantially higher if these were included. For example, query 10c, when allowed to run to completion, took 446.5 seconds. … **Extreme slowdowns**. Even among queries that completed, SQLite was often dramatically slower. Query 9d took 37.8 seconds on SQLite versus 1.6 seconds on Datalevin (24x). Query 19d took 20.8 seconds versus 5.7 seconds. Query families 9, 10, 12, 18, 19, 22, and 30 all show SQLite performing significantly worse, often by 10-50x. … 1. **Limited join order search**. SQLite uses exhaustive search for join ordering only up to a limited number of tables. Beyond that threshold, it falls back to heuristics that produce poor plans for complex queries. 2. **Weak statistics model**. SQLite's cardinality estimation is simpler than PostgreSQL's, which itself has well-documented weaknesses [1]. With fewer statistics to guide optimization, SQLite makes worse choices about which tables to join first and which access methods to use. 3. **No cost-based plan selection for complex cases**. For queries with many tables, SQLite's planner cannot explore enough of the plan space to find good join orderings. The result is plans that process orders of magnitude more intermediate rows than necessary. These limitations are architectural; they are not bugs likely to be fixed in a near-term release. They reflect design tradeoffs inherent in SQLite's goal of being a lightweight, embedded database. ## What this means for "SQLite in production" SQLite is excellent for what it was designed to be: an embedded database for applications with simple query patterns. It excels as a local data store, a file format, and a cache. For read-heavy workloads with straightforward queries touching a few tables, it works extremely well. But the production systems described above, e.g. CRM, EHR, e-commerce, authorization, analytics, are precisely where SQLite's query optimizer becomes a bottleneck. These are not hypothetical workloads, but the day-to-day reality of systems that serve businesses and users. The "SQLite in production" advocates often benchmark simple cases: key-value lookups, single-table scans, basic CRUD operations. On those workloads, SQLite does extremely well. But production systems grow. Schemas become more normalized as data integrity requirements increase. Questions become more compositional as business logic matures. And at that point, the query optimizer becomes the bottleneck, not the network round trip to a database server.
### The Classic Limitations **1. Single-writer concurrency.** SQLite uses a file-level lock. One writer at a time. In WAL (Write-Ahead Logging) mode, you get concurrent reads with a single writer, but that's it. If your app needs high write throughput from multiple connections, traditional SQLite chokes. **2. No built-in replication.** PostgreSQL has streaming replication. MySQL has binlog replication. SQLite has... copying the file. For any application that needs high availability, failover, or geographic distribution, SQLite was a non-starter. **3. Single-machine storage.** The database is a file on disk. No clustering, no sharding, no distributed storage. Your data lives and dies with that one machine. **4. No network access.** Unlike PostgreSQL or MySQL, which run as network services that accept connections from anywhere, SQLite is an embedded library. Your application talks to it directly — which means no shared access from multiple services. … ### Don't Use SQLite When: **1. You need high sustained write throughput from many sources.** If your app genuinely needs thousands of concurrent write transactions per second from different processes, use PostgreSQL or MySQL. Financial trading platforms, real-time analytics ingest, and IoT sensor data pipelines are examples. **2. You need complex multi-table transactions with strict SERIALIZABLE isolation across distributed systems.** SQLite doesn't have the distributed transaction capabilities of PostgreSQL or CockroachDB. **3. You need row-level security or advanced access control.** PostgreSQL's Row-Level Security (RLS) is battle-tested for multi-tenant scenarios where you want fine-grained access control within a single database. SQLite doesn't have this. **4. Your team already has strong PostgreSQL/MySQL expertise and infrastructure.** Don't switch to SQLite just because it's trendy. If your existing stack works well, the migration cost probably isn't worth it.
typecraft.dev
Why Everyone Is Talking About SQLite?**1. The Limits of SQLite: Where Does It Break?** Suggest using SQLite in production, and the first response you’ll hear is, *“Won’t that break?”* For years, the answer was a firm **yes**—at least for high-traffic, multi-user applications. And even today, SQLite has real **technical constraints** that developers need to understand before adopting it. **Where SQLite Struggles in Production** ### 🔴 **Write Contention** - SQLite uses a **single-writer model**, meaning only one transaction can write to the database at a time, while multiple transactions can read concurrently. In **WAL mode**(Write-Ahead Logging), concurrent reads and writes are possible, but only one write transaction can be committed at a time, which can still lead to contention under heavy write loads. **Workarounds:** - Enabling **WAL mode**(Write-Ahead Logging) helps by allowing multiple readers to access the database while a write transaction is ongoing. However, there’s still a ceiling because only one write operation can be performed at a time. When concurrent writes pile up, transactions start queuing, leading to slowdowns. This limitation makes SQLite less ideal for high-write workloads where multiple users frequently update data. - Some applications **batch writes**or offload them to a queue. - Enabling ### 🔴 **Scaling and Network Limitations** - SQLite was designed to be used **locally**, not over a networked filesystem like NFS or SMB. Doing so can cause **locking issues, performance degradation, and even data corruption.**Additionally, **SQLite does not scale horizontally**the way traditional databases do, as it lacks built-in support for distributed nodes and multi-writer replication. … These solutions help mitigate SQLite’s traditional limitations by adding replication and distributed capabilities, but they do not eliminate the single-writer model. While they enhance availability and redundancy, SQLite still operates with a single-writer architecture, meaning write contention can still be a concern in high-throughput environments. If high availability and true multi-writer clustering are key, a traditional database like Postgres or MySQL is
### 1. Read-Heavy Workloads If your app is 95% reads (like most SaaS apps), SQLite will embarrass your Postgres setup. No network roundtrips. No connection overhead. Just direct disk reads with an intelligent page cache. I've seen query times drop from 50ms to 0.5ms just by switching from Postgres to SQLite. … ### ❌ High Write Concurrency (But It's Not That Simple) SQLite uses a single writer model. One write at a time. But here's what the haters don't tell you: since Write-Ahead Logging (WAL) mode was introduced, SQLite can handle concurrent readers WHILE writing. No more blocking reads during writes. In WAL mode: - Writers don't block readers - Readers don't block writers - Multiple readers work simultaneously - Write performance improved dramatically … With WAL enabled and proper configuration, I've seen SQLite handle 500-1000+ writes/second on modern hardware while serving thousands of concurrent reads. Yes, Postgres can push higher numbers with multiple writers, but ask yourself: is your SaaS really doing more than 1000 writes per second? (Spoiler: it's not.) … ### ✅ But Here's What People Get Wrong: - "SQLite can't handle concurrent reads" – **Wrong.** It handles unlimited concurrent reads. - "SQLite doesn't support JSON" – **Wrong.** Full JSON support since 2015. - "SQLite can't do full-text search" – **Wrong.** FTS5 is excellent. - "SQLite databases corrupt easily" – **Wrong.** It's one of the most reliable storage formats ever created. … ## Top comments (50) ... Having separate servers for the application and the database is mainly because security concerns and different server requirement needs. It has less to do with what flavour of database you choose. Also Sqlite has no build-in authentication. This is a potential security risk. Sqlite is certainly a good sql database, and it is great the nice parts are getting promoted. Just be aware it is a tool like any other, it comes with flaws too. Yawar Amin • Jun 29 '25 > Sqlite has no build-in authentication. This is a potential security risk. Potentially, in the sense that if you don't give your database file the correct ownership and permissions on disk, it could be read by other users on the machine. Or if you let your machine accept incoming connections that can read files on disk, and can access the SQLite database file, they can potentially read your data. … - Rails 8 defaults to SQLite, so for my new projects I use SQLite as it's actually less work from the start!
www.nihardaily.com
SQLite in 2025: Why This “Simple” DB Powers Major AppsSQLite used to live in your browser or your phone. But in 2025, it's powering production apps — in the cloud. A new wave of tools is making it possible to use SQLite as a serverless, distributed, and production-ready database — without giving up its signature simplicity and speed. ... |**SELECT on 1 million rows**|~ **80 ms**|Typical SELECT query in a dataset with ~1 million rows. ([Toxigon][1])| |**INSERT operations per second**|~ **5,000 inserts/sec**|Under light/moderate load, single‐threaded or small concurrency. ([Toxigon][1])| |**UPDATE operations per second**|~ **3,000 updates/sec**|On the same 1M‐row dataset under similar test conditions. ([Toxigon][1])| … |**Small queries throughput: FTS5 vs LIKE vs others**|• FTS5: ~2,500–3,300 QPS, • LIKE operator: ~10–15 QPS, • “Normalized Table” / “Roaring Bitmaps”: much higher, tens of thousands QPS depending on filter complexity. ([Oldmoe's blog][3])|Tests on text search / tag filtering, showing the massive speed advantages when using full-text or optimized index/search methods vs naive LIKE. ... |**Many small HTTP requests / querying by ID**|• ~50,000 req/sec with pool size ~4–8 on certain implementations, • ~20-30k with lower pool/connections • Performance drops off with large pool sizes beyond CPU core count. ([GitHub][5])|These are web-server benchmarks running select by primary key, showing SQLite can handle surprisingly high request rates when optimized and with limited concurrency. ([GitHub][5])| |**Operation latency in GoatDB vs SQLite (simple operations)**|• Open DB (100k items): ~186 µs average, • Read item by ID: ~67 µs average, • Create table: ~1.1 ms, etc. ([GoatDB][6])| | … ## Can SQLite Be Used in Production? ... Let me address the elephant in the room. Every developer has heard the whispered warnings: "SQLite isn't for production," they say. "It can't scale," they insist. Well, I'm here to tell you that in 2025, those assumptions are not just outdated—they're costing developers opportunities to build simpler, faster applications. … ## Can SQLite Scale? The Truth About Performance Limits This is where the rubber meets the road. Scalability isn't just about handling more data—it's about handling more concurrent operations, more complex queries, and more demanding workloads without falling over. … - Unlimited concurrent readers - 100k SELECTs per second on properly tuned systems - In-memory caching for hot data **Write Performance:** Limited but Manageable - Single writer at a time (serialized writes) - SQLite can handle millions of QPS, and terabytes of data. However, our efforts to scale our Managed Jobs feature ran up against the one downfall of SQLite: many concurrent writers. … ### When Concurrency Becomes a Problem You'll hit SQLite's concurrency limits when: - More than 1,000 writes per second consistently - Write operations take longer than 100ms regularly - You have more than 50 concurrent writers - Real-time write consistency across multiple processes is critical … ### When SQLite Doesn't Win **Scale Requirements:** Thousands of concurrent writers will overwhelm SQLite's single-writer model **Distributed Systems:** Multiple servers need shared, consistent data access **Enterprise Features:** Advanced security, auditing, and compliance features may be limited **Team Collaboration:** Multiple developers working on shared data simultaneously
pages.cs.wisc.edu
SQLite: Past, Present, and Future• We optimize SQLite for analytical data processing. We identify key bottlenecks in SQLite and discuss the advantages and disadvantages of potential solutions. We integrate our optimizations into SQLite, resulting in overall 4.2X speedup on SSB. • We identify several performance measures specific to embeddable database engines, including library footprint … tinues to be the most widely used database engine in the world, the drastic changes in both hardware capabilities and software demands have exposed SQLite to a unique set of challenges. The expansion of hardware capabilities calls for a deeper evalua- tion into the underlying implementation of SQLite. Notably, SQLite generally does not use multiple threads, which limits its ability … it is likely that certain workloads, particularly those that include complex OLAP, would benefit from multithreading. Furthermore, SQLite’s row-oriented storage format and execution engine are suboptimal for many OLAP operations. In general, SQLite is con- sidered not to be competitive with state-of-the-art OLAP-focused … use cases of embeddable database engines, they are representative of the most common workloads. We identify key bottlenecks in SQLite’s OLAP performance, discuss the tradeoffs of potential so- lutions, and present the performance impact of our optimizations. Finally, we discuss the “footprint” of SQLite: the amount of memory
www.hendrik-erz.de
Why You Shouldn't Use SQLite - Hendrik ErzA second limitation is the file system. File systems have been built with reasonable file sizes in mind. That means: The limit for the files are even smaller than the limits for hard drive sizes. And then there’s speed: If you need an overview over the data you have you *must* at some point query every single piece of data in the SQLite file, and the bigger that file is, the longer this will take.
blog.driftingruby.com
Using SQLite in Production… ## Cons of Using SQLite in Production: **Concurrency Issues:** SQLite is not ideal for applications with a high level of concurrent write operations. It locks the entire database during a write operation, which can cause a bottleneck. *If we're working on a single threaded application since we're talking about smaller applications without too much traffic, this will not really be an issue. However, it does bring up a good point for any kind of background workers that occur asynchronous. Even though SQLite does a good job at handling transactions, it is easy to get ourselves into situations of Deadlocks when background jobs are trying to write to the database at the same time.* **Not Suited for Large Databases:** While SQLite supports database files up to 140 TB, it’s generally not the best choice for very large datasets due to potential performance issues. *Most databases will never reach 140TB. But this does not talk to file system limits. This is really a moot point since modern file systems exceed SQLite's limits, but if you're file system was improperly set (to something like Fat32), then you could easily reach the 4GB limit. Most likely, if your database was even getting to a fraction of this size, you'll have other issues to be concerned with; system resources, performance issues, etc.* **Limited Built-in Features:** Unlike more robust RDBMS like PostgreSQL or MySQL, SQLite doesn’t have a wide range of built-in tools or support for stored procedures, right out of the box. *It supports the important ones like JSON data types. Typically, we don't use stored procedures in Ruby on Rails applications since the ActiveRecord ORM is powerful enough in most cases.* **Limited Security:** SQLite doesn’t have built-in support for user management or access control, making it less secure than other RDBMS. *While this can be a critical aspect in some scenarios, the Ruby on Rails applications will likely not need this level of access control. However, it does speak to the vulnerability of the database as it will be much easier to make a copy of if the system is ever breached. … **No Client-Server Model:** SQLite doesn’t have a client-server model, which means it can’t be accessed by multiple users at the same time. *This is going to be a big issue if High Availability is a concern. Typically, we will have a load balancer as the SSL termination point and it will route the traffic to one of the available web servers. This is a deal breaker in many situations as using SQLite in a production setting could mean that you have downtime during deployments. You lose the ability for rolling deployments and have basically put all eggs in one basket. Depending on your deployment strategy, it could also mean that you're having to maintain a server instead of being able to manually or automatically provision and scale up new web servers.* … You can get a lot of value out of these services and it will be much easier to scale up in the future if you need to. I think that the biggest concern is the lack of High Availability This is a deal breaker for most applications that are going to be deployed in a production setting. However, for non-revenue generating applications or applications that are not mission critical, SQLite could be a good choice.
tinues to be the most widely used database engine in the world, the drastic changes in both hardware capabilities and software demands have exposed SQLite to a unique set of challenges. The expansion of hardware capabilities calls for a deeper evalua- tion into the underlying implementation of SQLite. Notably, SQLite generally does not use multiple threads, which limits its ability … use cases of embeddable database engines, they are representative of the most common workloads. We identify key bottlenecks in SQLite’s OLAP performance, discuss the tradeoffs of potential so- lutions, and present the performance impact of our optimizations. Finally, we discuss the “footprint” of SQLite: the amount of memory
sqlite.org
Quirks, Caveats, and Gotchas In SQLiteWhere this ends up causing problems is when developers do some initial coding work using SQLite and get their application working, but then try to convert to another database like PostgreSQL or SQL Server for deployment. If the application is initially taking advantage of SQLite's flexible typing, then it will fail when moved to another database that is more judgmental about data types. Flexible typing is a feature of SQLite, not a bug. Flexible typing is about freedom. Nevertheless, we recognize that this feature does sometimes cause confusion for developers who are accustomed to working with other databases that are more strict with regard to data type rules. In retrospect, perhaps it would have been less confusing if SQLite had merely implemented an ANY datatype so that developers could explicitly state when they wanted to use flexible typing, rather than making flexible typing the default. … # 4. Foreign Key Enforcement Is Off By Default SQLite has parsed foreign key constraints for time out of mind, but added the ability to actually enforce those constraints much later, with version 3.6.19 (2009-10-14). By the time foreign key constraint enforcement was added, there were already countless millions of databases in circulation that contained foreign key constraints, some of which were not correct. To avoid breaking those legacy databases, foreign key constraint enforcement is turned off by default in SQLite. … # 5. PRIMARY KEYs Can Sometimes Contain NULLs A PRIMARY KEY in an SQLite table is usually just a UNIQUE constraint. Due to an historical oversight, the column values of a PRIMARY KEY are allowed to be NULL. This is a bug, but by the time the problem was discovered there where so many databases in circulation that depended on the bug that the decision was made to support the buggy behavior moving forward. You can work around this problem by adding a NOT NULL constraint on each column of the PRIMARY KEY. Exceptions: - The value of an INTEGER PRIMARY KEY column must always be a non-NULL integer because the INTEGER PRIMARY KEY is an alias for the ROWID. If you try to insert a NULL into an INTEGER PRIMARY KEY column, SQLite automatically converts the NULL into a unique integer. - The WITHOUT ROWID and STRICT features was added after this bug was discovered, and so WITHOUT ROWID and STRICT tables work correctly: They disallow NULLs in the PRIMARY KEY. … # 7. SQLite Does Not Do Full Unicode Case Folding By Default SQLite does not know about the upper-case/lower-case distinction for all unicode characters. SQL functions like upper() and lower() only work on ASCII characters. There are two reasons for this: 1. Though stable now, when SQLite was first designed, the rules for unicode case folding were still in flux. That means that the behavior might have changed with each new unicode release, disrupting applications and corrupting indexes in the process. 2. The tables necessary to do full and proper unicode case folding are larger than the whole SQLite library. … This misfeature means that a misspelled double-quoted identifier will be interpreted as a string literal, rather than generating an error. It also lures developers who are new to the SQL language into the bad habit of using double-quoted string literals when they really need to learn to use the correct single-quoted string literal form. In hindsight, we should not have tried to make SQLite accept MySQL 3.x syntax, and should have never allowed double-quoted string literals. However, there are countless applications that make use of double-quoted string literals and so we continue to support that capability to avoid breaking legacy. As of SQLite 3.27.0 (2019-02-07) the use of a double-quoted string literal causes a warning message to be sent to the error log. … The same SQL statement will fail on every other SQL implementation that we know of due to the use of keywords "union", "true", and "with" as identifiers. The ability to use keywords as identifiers promotes backwards compatibility. As new keywords are added, legacy schemas that just happen to use those keywords as table or column names continue to work. However, the ability to use a keyword as an identifier sometimes leads to surprising outcomes. For example:
www.telerik.com
A Primer on Tailwind CSS: Pros, Cons & Real-World Use Cases## Things to Keep in Mind with TailwindTMWhile Tailwind CSS offers numerous benefits, it’s essential to understand its potential challenges and quirks. One common concern is that Tailwind’s utility-first approach can make HTML files appear verbose and cluttered, especially when components require many utility classes. This can impact readability, particularly in larger codebases, unless there is a clear structure or documentation. … While this example effectively uses Tailwind’s utility classes to achieve a detailed design, it demonstrates the potential verbosity of the utility-first approach. The HTML is readable but cluttered, making it challenging to identify the component’s structure quickly. Tailwind offers developers the capability to use an @apply directive to consolidate utility classes into reusable CSS classes. Using the `@apply` directive on the verbose card example above, we can simplify the HTML by moving the repeated utility classes into reusable CSS classes. This improves readability and makes the component more maintainable and scalable in larger projects. … In addition to dealing with verbose class definitions, new developers may face a learning curve when working with Tailwind for the first time. The framework’s extensive set of utility classes and naming conventions can take time to memorize and fully understand. Although Tailwind’s documentation is comprehensive and helpful, onboarding might initially slow down developers unfamiliar with utility-first frameworks.